- 投稿日:2020-10-17T16:14:40+09:00
Doma入門 - トランザクション
はじめに
データベースアクセスに欠かせないトランザクション機能についてポイントを紹介します。
この記事では、Doma 2.44.0を前提とします。
Domaの他の機能紹介についてはDoma入門もお読みください。
最初に検討すること
トランザクションに関して最初に検討すべきことがあります。
それは、利用しているフレームワーク(SpringFrameworkなど)がトランザクション機能を提供しているかどうかです。提供している場合、まずはそちらの利用を検討ください。SpringFrameworkを使う場合
SpringFrameworkはトランザクション機能を提供します。
org.seasar.doma.jdbc.Config
の実装クラスのgetDataSource
メソッドでは、org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
を使ってラップしたDataSource
を返すように設定してください。これが非常に重要です。上記対応を行った上でこのガイドを参考にSpringのコンポーネントのクラスやメソッドに
@Transactional
を付与すればトランザクションを利用できます。doma-spring-bootを使えば上記の
DataSource
のラッピングは自動で行われます。doma-spring-bootを使ってSpringFrameworkのトランザクション機能を利用しているサンプルアプリとしてはspring-boot-jpetstoreがあります。Quarkusを使う場合
Quarkusはトランザクション機能を提供します。
org.seasar.doma.jdbc.Config
の実装クラスのgetDataSource
メソッドでは、Quarkusのコネクションプール実装であるAgroalで管理されたDataSource
を返してください。上記対応を行った上でこのドキュメントにあるようにCDIのコンポーネントのクラスやメソッドに
@Transactional
を付与すればトランザクションを利用できます。Quarkus Extension for Domaを使えば上記の
DataSource
の設定は自動で行われます。Quarkus Extension for Domaを使ってQuarkusのトランザクション機能を利用しているサンプルアプリとしてはquarkus-sampleがあります。トランザクション機能を提供するフレームワークを使わない場合
Domaのローカルトランザクションの利用を検討してください。
Domaのローカルトランザクションの特徴は次の通りです。
ThreadLocal
を使ってスレッドごとにコネクションを管理する- 名前の通りローカルトランザクションなので扱えるリソースは1つだけ(グローバルトランザクションのように2フェーズコミットの機能はない)
- 手続的なAPIである
org.seasar.doma.jdbc.tx.TransactionManager
を提供する(@Transactional
のような宣言的なAPIではない)- JDBCのセーブポイント機能を提供する
使い方は次の節で述べます。
Domaのローカルトランザクション
動くコードはgetting-startedにありますが、ここでは重要な部分を抜粋して示します。
Main.javapublic class Main { public static void main(String[] args) { var config = createConfig(); var tm = config.getTransactionManager(); // setup database var appDao = new AppDaoImpl(config); tm.required(appDao::create); // read and update // ④トランザクションマネージャーのメソッドにラムダ式を渡す tm.required( () -> { var repository = new EmployeeRepository(config); var employee = repository.selectById(1); employee.age += 1; repository.update(employee); }); } private static Config createConfig() { var dialect = new H2Dialect(); // ①トランザクション対応のデータソースを作る var dataSource = new LocalTransactionDataSource("jdbc:h2:mem:tutorial;DB_CLOSE_DELAY=-1", "sa", null); var jdbcLogger = new Slf4jJdbcLogger(); // ②トランザクションマネージャーを作る var transactionManager = new LocalTransactionManager(dataSource, jdbcLogger); // ③Configの実装クラスから上記の①や②で作ったインスタンスを返せるようにする return new DbConfig(dialect, dataSource, jdbcLogger, transactionManager); } }①トランザクション対応のデータソースを作る
LocalTransactionDataSource
をインスタンス化します。
この例では接続URLなどをコンストラクタで受け取っていますが、DataSource
インスタンスを受け取るコンストラクタも持っています。②トランザクションマネージャーを作る
上記の①で作った
LocalTransactionDataSource
のインスタンスをコンストラクタに渡してLocalTransactionManager
をインスタンス化します。③Configの実装クラスから上記の①や②で作ったインスタンスを返せるようにする
①と②で作ったインスタンスを
Config
の実装クラスであるDbConfig
のコンストラクタに渡してインスタンス化します。④トランザクションマネージャーのメソッドにラムダ式を渡す
ここの
tm
は②で作ったLocalTransactionManager
のインスタンスです。
tm
のrequired
メソッドにトランザクション内で扱いたい処理をラムダ式で渡すことでトランザクションを実行できます。
required
メソッドはトランザクションがまだ開始されていなかったら開始するメソッドで、他に常に新規にトランザクションを開始するrequiresNew
メソッドやトランザクションを一旦停止するnotSupported
メソッドなどがあります。これらのメソッドはネストして使えます。ラムダ式から例外をスローするか
setRollbackOnly
メソッドを呼び出すかするとトランザクションはロールバックされます。それ以外ではコミットされます。1つ注意点ですが、ローカルトランザクションの設定をした場合、Domaによる全てのデータベースアクセスは原則的に
TransactionManager
経由で行う必要があります。そうしない場合、例外が発生します。おわりに
Domaでトランザクションを利用するポイントを紹介しました。
Domaを使っていてトランザクションがうまく動いていないなと思ったら、この記事をはじめリンク先の記事やサンプルも参考にしてもらえればと思います。
- 投稿日:2020-10-17T15:59:45+09:00
Java/JavaScript/C#で暗号化する
各言語で暗号化。
やってみると、暗号化後の文字列が各言語で違ってしまったりして、意外とハマったりします。
ここでは、Java/JavaScript/C#を取り上げてみます。暗号化
Java
まずは、Java。
Crypto.javaimport java.nio.charset.StandardCharsets; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public final class Crypto { private static final String KEY = "1234567890abcdef"; private static final String IV = "abcdef1234567890"; private Crypto() { } private static Cipher createCipher(int mode) throws Exception { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec key = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES"); IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); cipher.init(mode, key, iv); return cipher; } public static String encrypt(String text) { try { Cipher cipher = createCipher(Cipher.ENCRYPT_MODE); Base64.Encoder encoder = Base64.getEncoder(); return encoder.encodeToString(cipher.doFinal(text.getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new RuntimeException(e); } } public static String decrypt(String text) { try { Cipher cipher = createCipher(Cipher.DECRYPT_MODE); Base64.Decoder decoder = Base64.getDecoder(); return new String(cipher.doFinal(decoder.decode(text))); } catch (Exception e) { throw new RuntimeException(e); } } }JavaScript
次にJavaScript。実行環境はNode.jsです。
Crypto.jsconst crypto = require('crypto') const KEY = '1234567890abcdef' const IV = 'abcdef1234567890' function createCipher(mode) { return crypto[mode]('aes-128-cbc', KEY, IV) } function encrypt(text) { const cipher = createCipher('createCipheriv') const encrypted = cipher.update(text); return Buffer.concat([encrypted, cipher.final()]).toString('base64') } function decrypt(text) { const buf = Buffer.from(text, 'base64') const cipher = createCipher('createDecipheriv') const decrypted = cipher.update(buf); return Buffer.concat([decrypted, cipher.final()]).toString('utf-8') }C#
最後にC#。
Crypto.csusing System; using System.Security.Cryptography; using System.Text; public sealed class Crypto { private const string KEY = "1234567890abcdef"; private const string IV = "abcdef1234567890"; private Crypto() { } private static AesManaged CreateAesManaged() { AesManaged aes = new AesManaged(); aes.KeySize = 256; aes.BlockSize = 128; aes.Mode = CipherMode.CBC; aes.IV = Encoding.UTF8.GetBytes(IV); aes.Key = Encoding.UTF8.GetBytes(KEY); aes.Padding = PaddingMode.PKCS7; return aes; } public static string Encrypt(string text) { AesManaged aes = CreateAesManaged(); byte[] byteText = Encoding.UTF8.GetBytes(text); byte[] encryptText = aes.CreateEncryptor().TransformFinalBlock(byteText, 0, byteText.Length); return Convert.ToBase64String(encryptText); } public static string Decrypt(string text) { AesManaged aes = CreateAesManaged(); byte[] src = System.Convert.FromBase64String(text); byte[] dest = aes.CreateDecryptor().TransformFinalBlock(src, 0, src.Length); return Encoding.UTF8.GetString(dest); } }結果
暗号化(
encrypt
)すると、以下の結果になります。
- ""(空文字)=> 0e8Yyuobp55/giEyC01lbg==
- alz1590-'$ => cMgaJ1X/cVENp+rnMo/8Kw==
- あむんアムンアムン亜無漚錡?? => jXw9bzISBZsndhcrJx/gJU7ltBzSp34xn6CL3xBbNE2yC0FhQBisP27WOXan/W2o
復号(
decrypt
)すると、元の文字列が得られます。補足
IV
は初期化ベクトルです。同じデータであっても違う暗号文にするための文字列です。KEY
/IV
は長さに決まりがあるので注意。
- 投稿日:2020-10-17T15:04:33+09:00
Log4J Asyncやったら遅くなるんだけど
みんな大好きLog4J。この記事は非同期の設定
作者はロギングのシステムについてはあんまりわかってない。
WEBでまよったりして動かなかったので記事まとめる。結論
なんか私の環境ではAsyncじゃないほうが速い。
動機
WEBアプリケーションのボトルネックはロギングとDBなので速いものを求めた。
公式
http://logging.apache.org/log4j/2.x/manual/async.html
6-68倍速とのこと。すごい。
監査証跡ログとかExceptionログとかに使わないほうが良いよ!っていってるけどまぁ大丈夫だよね。使う。要求
IPAの文書に従うと(プロジェクトメンバーが読んでくれてスペック教えてくれた)、
* SQLのログ
* アクセスログ
* アプリのログ(自由形式?)
を作っておけとのこと。
アクセスログはWEBサーバーで出しても良いんだろうけどアプリ側でも出すことにする
よって一旦3つのロガーを設定する例を考えた書き方
Asyncで囲むAsyncLoggerと書く
前者だと私が試した感じでうまく行かなかった。なんで?→AsyncLogger使う
RollingRandomAccessFileAppenderとFileAppenderとRollingFileAppender
RandomAccessFileが速いらしいので採用
log4j2.xml
実際わからんよね。むずい。
の前にlog4j.properties
log4j.properties作って置かないとおこられるので雑に置く。多分FWのVert.xの要求だとおもうんだけど追いかけていない
log4j.rootLogger=ERROR, console log4j.logger.xxx=ERROR, console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d [%-5p-%c] %m%nlog4j.xml
愚直に書くよ
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingRandomAccessFile name="AppLog" fileName="logs/app.log" filePattern="logs/old/app-%d{yyyy-MM-dd}.log.gz" ignoreExceptions="false"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <TimeBasedTriggeringPolicy/> </RollingRandomAccessFile> <RollingRandomAccessFile name="AccessLog" fileName="logs/access.log" filePattern="logs/old/access-%d{yyyy-MM-dd}.log.gz" ignoreExceptions="false"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <TimeBasedTriggeringPolicy/> </RollingRandomAccessFile> <RollingRandomAccessFile name="ErrorLog" fileName="logs/error.log" filePattern="logs/old/error-%d{yyyy-MM-dd}.log.gz" ignoreExceptions="false"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <TimeBasedTriggeringPolicy/> </RollingRandomAccessFile> <RollingRandomAccessFile name="SQLLog" fileName="logs/sql.log" filePattern="logs/old/sql-%d{yyyy-MM-dd}.log.gz" ignoreExceptions="false"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <TimeBasedTriggeringPolicy/> </RollingRandomAccessFile> <Async name="AppAsync"> <AppenderRef ref="Console"/> </Async> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="AppAsync"/> </Root> <AsyncLogger level="debug" name="access-log"> <AppenderRef ref="AccessLog"/> </AsyncLogger> <AsyncLogger level="debug" name="error-log"> <AppenderRef ref="ErrorLog"/> </AsyncLogger> <AsyncLogger level="debug" name="sql-log"> <AppenderRef ref="SQLLog"/> </AsyncLogger> </Loggers> </Configuration>パフォーマンス
ab
コマンドで雑に叩く
セレロンの超遅いファイルサーバーマシンで確認非同期
Requests per second: 347.96 [#/sec] (mean)同期
Requests per second: 2830.04 [#/sec] (mean)同期のほうが速いですやん。嘘やん。なんか間違っている気がする。
計測大事
同期のほうが速いという状況。よくわからん。ちゃんと計測しないとだめですねー
以上です。
- 投稿日:2020-10-17T14:43:36+09:00
JavaでTODOアプリを制作しよう11 存在しないIDのTODOにアクセスした時の例外処理
こんにちは。
Java + SpringでTODOアプリを作るシリーズですが、今回からいよいよ例外処理を実装していきます。
まずは例外処理に慣れるためにも比較的簡単なところから始めたいので、存在しないIDにアクセスした時の例外処理を見てみましょう!
TODOアプリ作成リンク集
1: [超基礎の理解] MVCの簡単な説明
2: [雛形を用意する] Spring Initializrで雛形を作ってHello worldしたい
3: [MySQLとの接続・設定・データの表示] MySQLに仮のデータを保存 -> 全取得 -> topに表示する
4: [POST機能] 投稿機能の実装
5: [PATCH機能] TODOの表示を切り替える
6: [JpaRepositoryの簡単な使い方] 検索機能の実装
7: [Thymeleaf テンプレートフラグメントで共通化] Headerの作成
8: [PUT機能] 編集機能の実装
9: [微調整]TODOの表示を作成日時が新しい順にソートする + 期日のデフォルトを今日の日付にする
10: [springで例外処理] 例外処理についての簡単なまとめ
11: [springで例外処理] 存在しないIDのTODOにアクセスした時の例外処理(今ここ)例外処理がどう処理されるか考えてみる
さて前回の記事では例外処理とは何なのかについて簡単に説明しました。
要約すると
ユーザーが製作者の意図しないリクエストをした時(TODO登録時に規定数以上の文字を入力する、存在しないURLにアクセスしようとする ...etc)、特定のエラーページへ誘導するすることでセキュリティ面・ユーザビリティの向上を図る。
ということになります。
例外処理の流れ
今回実装するのは存在しないIDのTODOにアクセスをリクエストした際ですので・・・
まずはコントローラー確認
com/example/todo/TodoController.java@GetMapping("/edit/{id}") public String showEdit(Model model, @PathVariable("id") long id ) { TodoEntity editTarget = todoService.findTodoById(id); model.addAttribute( "editTarget" , editTarget); return "edit"; }この
showEdit
呼び出し時の例外処理となります。todoService.findTodoById(id)
で Serviceクラスを経由してTODOを取得するわけですが、findTodoById()
をみてみましょう。ServiceクラスのfindTodoById()の確認
com/example/todo/TodoService.javapublic TodoEntity findTodoById(Long todoId) { Optional<TodoEntity> todoResult = todoRepository.findById(todoId); return todoResult.get(); }RepositoryからID検索をしてTODOを取得してOptional型で返しています。
しかしここで存在しないIDで検索しようとするとエラーで落ちてしまうので、このタイミングで
もしtodoResultが空だったら特定のExceptionクラスへ誘導する
様にすればいいでしょう!
特定のExceptionクラス?
com/example/todo/exception/TodoNotFoundException.javapackage com.example.todo.exception; public class TodoNotFoundException extends RuntimeException{ }
exception
というディレクトリを新しく作り、その中にTodoNotFoundException
というクラスを作ります。
extends
によってこのクラスはRuntimeException
を継承していることになります。
継承したことによってこのクラスでもRunttimeException
のメソッドが使用できるようになります。(今回は使用しないですが・・・)findTodoById()内でtodoResultが空だった時にTodoNotFoundExceptionクラスへ誘導する
com/example/todo/TodoService.javapublic TodoEntity findTodoById(Long todoId) { Optional<TodoEntity> todoResult = todoRepository.findById(todoId); todoResult.orElseThrow(TodoNotFoundException::new); return todoResult.get(); }returnの前に追加した1行を見てみましょう!
Optional.orElseThrow(Exception名::new)
とすることでOptional型が空だった時に指定したクラスへ処理を飛ばすことが出来ます。(今回は先ほどつくったTodoNotFoundException
)ちなみに
Exception名::new
の部分はメソッド参照と呼ばれていて、メソッドの引数内としてメソッドを参照するものとなります。こちらの記事が参考になるので見てみてください。
TodoNotFoundExceptionを管理するクラスを作る。
先ほど作った
TodoNotFoundException
はRuntimeException
を継承しただけで中身は空でしたね。com/example/todo/exception/TodoNotFoundException.javapackage com.example.todo.exception; public class TodoNotFoundException extends RuntimeException{ }ここで@ControllerAdviceというアノテーションを使って、TodoNotFoundExceptionが呼ばれた時(=スローされた時)の処理を実装していきます。
com/example/todo/exception/TodoControllerAdvice.javapackage com.example.todo.exception; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @Slf4j @ControllerAdvice public class TodoControllerAdvice { @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(TodoNotFoundException.class) public String idNotFound() { log.warn("指定されたTODOが見つかりません。"); return "error/404.html"; } }
exception
ディレクトリ内にこんなクラスを作ってみましょう。上から順に説明すると・・・
- @Slf4j
- このアノテーションを使う事で、ログを表示できる様になります。log.warn("内容")やlog.info("内容")でターミナルにログを表示できます。
- @ControllerAdvice
- このクラスがControllerAdviceである事を示します。TodoControllerで例外が発生した時にどのように処理するかを設定できるようになります。
- @ResponseStatus(HttpStatus.NOT_FOUND)
- Not_FoundとしてHttpStatusを返します。
- @ExceptionHandler(TodoNotFoundException.class)
- ここが大事なのですが、このアノテーションを用いることによってTodoController内の処理でTodoNotFoundExceptionが発生した際にこのidNotFound()に処理がくるように指定しています。この関数内ではエラーログを出力しつつ、error/404.htmlを表示させるようにしています。
エラーページの作成
templates/error/404.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>404</title> </head> <body> 404! </body> </html>本来ならエラー内容を表示させるべきですが、今回は簡素に404!と表示させるだけにします。
今回実装した例外処理の流れまとめ
さて一気に説明したので混乱しているかもしれませんが、今回の処理をまとめると以下になります。
・ユーザーがTodo編集をリクエスト(800/edit/100)
↓
・コントローラがリクエストに応じてサービスクラスへ処理をまわす(findTodoById(100))
↓
・ID = 100のTODOは存在しないので
TodoNotFoundException
が呼ばれる。↓
・@ControllerAdviceがついた
TodoControllerAdvice
が「指定されたTODOが見つかりません」というログを出すと共に404.htmlへ遷移させる。こんな感じです!
ちょっと文章だとわかりづらいかもしれないので、こちらの記事を読んでみると良いと思います。
次回も続いて例外処理の実装を進めていきます!
- 投稿日:2020-10-17T14:38:15+09:00
【Java】 キャッシュの影響でアップロードした画像が更新されない問題
概要
作成中のwebアプリで画像をアップロードしたはいいものの、アップロードした画像が画面で中々更新されない問題で詰まったので、自分用にメモします。
原因としてはキャッシュが悪さをしていて、更新前の画像をそのまま読み込んでしまっているためと思われます。解決方法
結論から言ってしまえば、jsp側で表示している画像の"<img src=~"に?(何かしらの値)を付けて、ブラウザに違う画像と認識させるだけで解決する事ができました。
test.jsp//変更前 <img src="static/img/test.jpg" alt="test.jpg"> //変更後 <img src="static/img/test.jpg?(任意の動的パラメーター)" alt="test.jpg">画像のURLがまったく同じで何も変更がないと、ブラウザは「キャッシュを使用すればいい」と判断してしまうので、更新が反映されなくなっていたようです。
パラメーターをlastModifiedで付与する
任意の固有のパラメーターなので何でも良いのですが、せっかくなので自動で値を取得して付与するようにしてみます。
アップロードした日付と時間を、lastModifiedで取得し付与します。Controller.java// 対象とするファイルを選択する File file = new File("static/img/test.jpg"); // ファイルの最終更新日時を取得し、jspで使用可能にする model.addAttribute("file", file.lastModified());test.jsp<img src="/static/img/test.jpg?${file}" alt="test.jpg">これでファイルを更新しても、更新後のものが読み込まれるようになります。
参考
・Spring Bootで静的リソースのブラウザキャッシュ対応
・「キャッシュのせいです」とは言わせない!更新されたファイルだけ確実にキャッシュを無効化させる方法
・更新されたJS、CSS、画像のみブラウザキャッシュを破棄して読み込ませる
- 投稿日:2020-10-17T14:20:08+09:00
ARC104のD Multiset MeanをScala、Java、C++、Ruby、Perl、Elixirで解く
AtCoder ARC104の4問目をScala、C++、Java、Ruby、Perl、Elixirで解きました。言語ごとの処理速度の比較ができました。
結果
言語 結果 Scala TLE C++ AC 1830ms Java AC 2995ms JavaっぽいScala AC 2991ms Ruby TLE Perl TLE Elixir TLE 競技プログラミングにおける言語の選択と処理速度の関係は以下のツイートに言及がありました。
https://twitter.com/chokudai/status/1171321024224186369
このツイートによると、C++、Javaは大丈夫だけど、他の言語は計算量がシビアな問題には厳しいようです。
競技中は解法を見つけ出すのに時間がかかり、わかったあとはScalaで書いたものの、バグ取りに手間取って、AC(正解)に至りませんでした。競技後も続けたものの、どうしても実行時間制限4秒のTLEを解消できませんでした。
あきらめて同じロジックをC++で書いたらあっさりACを取れました。
そこで、せっかくなのでいろんな言語で書いてみて、AC取れるかを比較した結果です。
Scalaは最初はTLEだったものの、JavaでAC取ったあとにJavaを翻訳したようなScalaを書いたらScalaでもACとれました。
この記事の残りは各言語のソースコードです。
Scala
Array
を使うなど中途半端にJavaっぽいScalaコードですが、TLEを解消できずにあきらめたコードです。object Main extends App { val sc = new java.util.Scanner(System.in); val n, k, m = sc.nextInt(); val table = (0 until n).map(i => Array.fill(i * (i+1) * k / 2 + 2)(0)); table(0)(0) = 1; (1 until n).foreach { i => val j_max = i * (i+1) * k / 2; val t1 = table(i - 1); val t2 = table(i); (0 to j_max).foreach { j => val a1 = (((j - (i - 1) * i * k / 2) + i - 1) / i) max 0; val a2 = (j / i) min k; val s = (a1 to a2).map { a => t1(j - a * i).toLong; }.sum % m; t2(j) = s.toInt; } } val table2 = Array.fill(n+1)(0); (1 to n).foreach { x => if (x <= (n + 1) / 2) { val (n1, n2) = (x - 1, n - x); val s = (0 to (n1 * (n1+1) * k / 2)).map { i => table(n1)(i).toLong * table(n2)(i) % m; }.sum % m; val answer = ((m.toLong + s * (k+1) - 1) % m).toInt; table2(x) = answer; println("%d".format(answer)); } else { val answer = table2(n + 1 - x); println("%d".format(answer)); } } }後述のJavaでAC取れたあと、JavaをScalaに翻訳したようなコードを書いたところ、Javaとほぼ同じ2991msでした。Scalaでこういうコードを書くぐらいならJavaでいいわけで、Scalaは計算量が厳しい問題には向いていないという結論です。Scalaは私にとって書きやすいので今後も使い分ければよいです。
object Main extends App { val sc = new java.util.Scanner(System.in); val n, k, m = sc.nextInt(); val table = new Array[Array[Int]](n); table(0) = new Array[Int](2); table(0)(0) = 1; var i: Int = 1; while (i < n) { val j_max = i * (i+1) * k / 2; val t1 = table(i - 1); val t2 = new Array[Int](i * (i+1) * k / 2 + 2); table(i) = t2; var j: Int = 0; while (j <= j_max) { val a1 = (((j - (i - 1) * i * k / 2) + i - 1) / i) max 0; val a2 = (j / i) min k; var s: Long = 0L; var a: Int = a1; while (a <= a2) { s += t1(j - a * i).toLong; a += 1; } t2(j) = (s % m).toInt; j += 1; } i += 1; } val table2 = new Array[Int](n+1); var x: Int = 1; while (x <= n) { if (x <= (n + 1) / 2) { val (n1, n2) = (x - 1, n - x); var s: Long = 0L; var i: Int = 0; var max = (n1 * (n1+1) * k / 2); while (i <= max) { s += table(n1)(i).toLong * table(n2)(i) % m; i += 1; } s = s % m; val answer = ((m.toLong + s * (k+1) - 1) % m).toInt; table2(x) = answer; println("%d".format(answer)); } else { val answer = table2(n + 1 - x); println("%d".format(answer)); } x += 1; } }C++
同じロジックをC++で書き直したところ、1830msでAC取れました。
#include <bits/stdc++.h> using namespace std; int main() { int n, k, m; cin >> n >> k >> m; vector<vector<int>> table(n); table[0] = vector<int>(2); table[0][0] = 1; for (int i = 1; i < n; i++) { auto t1 = table[i-1]; auto t2 = vector<int>(i * (i+1) * k / 2 + 2); int j_max = i * (i+1) * k / 2; for (int j = 0; j <= j_max; j++) { int a1 = ((j - (i - 1) * i * k / 2) + i - 1) / i; if (a1 < 0) a1 = 0; int a2 = j / i; if (a2 > k) a2 = k; long s = 0; for (int a = a1; a <= a2; a++) { s += t1[j - a * i]; } s = s % m; t2[j] = (int)s; } table[i] = t2; } auto table2 = vector<int>((n + 1) / 2 + 1); for (int x = 1; x <= (n + 1) / 2; x++) { int n1 = x - 1; int n2 = n - x; long s = 0; for (int i = 0; i <= n1 * (n1+1) * k / 2; i++) { s += (long)table[n1][i] * table[n2][i] % m; } int answer = (int)(((long)m + s * (k+1) - 1) % m); table2[x] = answer; printf("%d\n", answer); } for (int x = (n + 1) / 2 + 1; x <= n; x++) { printf("%d\n", table2[n + 1 - x]); } }Java
C++でACが取れ、解法が間違ってないことがわかりましたので、次はJavaで書きました。2995msでAC取れました。C++よりは遅いものの、同じJVMのScalaよりも高速です。JVMでできるということは、ScalaでもJavaっぽい書き方にすればできるはずと考えたところ、ScalaでもACが取れたのは先述のとおりです。
import java.util.Scanner; class Main { public static void main(String[] args) { var sc = new Scanner(System.in); var n = sc.nextInt(); var k = sc.nextInt(); var m = sc.nextInt(); var table = new int[n][]; table[0] = new int[2]; table[0][0] = 1; for (int i = 1; i < n; i++) { var t1 = table[i-1]; var t2 = new int[i * (i+1) * k / 2 + 2]; int j_max = i * (i+1) * k / 2; for (int j = 0; j <= j_max; j++) { int a1 = ((j - (i - 1) * i * k / 2) + i - 1) / i; if (a1 < 0) a1 = 0; int a2 = j / i; if (a2 > k) a2 = k; long s = 0; for (int a = a1; a <= a2; a++) { s += t1[j - a * i]; } s = s % m; t2[j] = (int)s; } table[i] = t2; } var table2 = new int[(n + 1) / 2 + 1]; for (int x = 1; x <= (n + 1) / 2; x++) { int n1 = x - 1; int n2 = n - x; long s = 0; for (int i = 0; i <= n1 * (n1+1) * k / 2; i++) { s += (long)table[n1][i] * table[n2][i] % m; } int answer = (int)(((long)m + s * (k+1) - 1) % m); table2[x] = answer; System.out.printf("%d\n", answer); } for (int x = (n + 1) / 2 + 1; x <= n; x++) { System.out.printf("%d\n", table2[n + 1 - x]); } } }Ruby
TLEでした。
n, k, m = gets.strip.split(" ").map(&:to_i) table = [[1]] for i in 1..n-1 t1 = table[i-1] t2 = [] for j in 0 .. i * (i+1) * k / 2 a1 = ((j - (i - 1) * i * k / 2) + i - 1) / i; a1 = 0 if a1 < 0 a2 = j / i; a2 = k if a2 > k s = 0; for a in a1 .. a2 s += t1[j - a * i] end s = s % m t2.push(s) end table.push(t2) end table2 = [0] for x in 1 .. (n + 1) / 2 n1 = x - 1 n2 = n - x s = 0 for i in 0 .. n1 * (n1+1) * k / 2 s += table[n1][i] * table[n2][i] % m; end answer = (m + s * (k+1) - 1) % m table2.push(answer) p answer end for x in (n + 1) / 2 + 1 .. n p table2[n + 1 - x] endPerl
TLEでした。
use strict; use warnings; use integer; my $nkm = <STDIN>; $nkm =~ s/\n\z//; my ($n, $k, $m) = split(/ /, $nkm); my $table = [[1]]; for (my $i = 1; $i < $n; $i++) { my $t1 = $table->[$i - 1]; my $t2 = []; my $j_max = $i * ($i + 1) * $k / 2; for (my $j = 0; $j <= $j_max; $j++) { my $a1 = (($j - ($i - 1) * $i * $k / 2) + $i - 1) / $i; $a1 = 0 if $a1 < 0; my $a2 = $j / $i; $a2 = $k if $a2 > $k; my $s = 0; for (my $aa = $a1; $aa <= $a2; $aa++) { $s += $t1->[$j - $aa * $i]; } $s = $s % $m; push(@$t2, $s); } push(@$table, $t2); } my $table2 = [0]; for (my $x = 1; $x <= ($n + 1) / 2; $x++) { my $n1 = $x - 1; my $n2 = $n - $x; my $s = 0; for (my $i = 0; $i <= $n1 * ($n1+1) * $k / 2; $i++) { $s += $table->[$n1]->[$i] * $table->[$n2]->[$i] % $m; } my $answer = ($m + $s * ($k+1) - 1) % $m; push(@$table2, $answer); printf("%d\n", $answer); } for (my $x = ($n + 1) / 2 + 1; $x <= $n; $x++) { printf("%d\n", $table2->[$n + 1 - $x]); }Elixir
TLEでした。
defmodule Main do def main do [n, k, m] = IO.read(:line) |> String.trim() |> String.split(" ") |> Enum.map(&String.to_integer/1) table = Enum.reduce(1 .. n, [[1, 0]], fn i, acc -> [t1 | _] = acc j_max = div(i * (i+1) * k, 2); t2 = Enum.map(0 .. j_max, fn j -> a1 = max(div((j - div((i - 1) * i * k, 2)) + i - 1, i), 0) a2 = min(div(j, i), k) rem(Enum.reduce(a1 .. a2, 0, fn a, acc3 -> acc3 + Enum.at(t1, j - a * i) end), m) end) [Enum.reverse(t2) | acc] end) |> Enum.reverse table2 = (1 .. div(n + 1, 2)) |> Enum.map(fn x -> [n1, n2] = [x - 1, n - x] s = Enum.reduce(0 .. div(n1 * (n1 + 1) * k, 2), 0, fn i, acc -> rem((table |> Enum.at(n1) |> Enum.at(i)) * (table |> Enum.at(n2) |> Enum.at(i)) + acc, m) end) rem(m + s * (k+1) - 1, m) end) (1 .. n) |> Enum.each(fn x -> if x <= div(n + 1, 2) do IO.puts(Enum.at(table2, x - 1)) else IO.puts(Enum.at(table2, n - x)) end end) end end
- 投稿日:2020-10-17T13:43:21+09:00
【Java】com.sun.glass.WindowEventがimportされていてウインドウが閉じない
開発環境
IDE: Eclipse photon 4.8
MacBook: Catalina 10.15.7
言語: java8閉じるボタンでウインドウを閉じるためのコード
addWindowListener(new WindowAdapter() { // @SuppressWarnings("unused") public void windowClosing (WindowEvent e) { dispose(); System.exit(0); } });
@SuppressWarnings("unused")
のところは、Eclipseが提案してきたコメント。これを挿入すればエラーは表示されなくなったけど、使用していないよと明示してJVMに教えたげてるので、使われていない変数があってもエラーとして認識しなくてもいいよ。書いた人もわかっているよ
ということでエラーを表示させなくするためのコメント。つまり、解決してないってこと!
当然、ウインドウを閉じることはできない。でも教材通りに書いてるつもりなんだけど。
Eclipseに頼ってたらちゃんと理解できない
// この部分でエラーが発生 public void windowClosing (WindowEvent e) {Eclipseのエディターには
com.sun.glass.events.WindowEventをインポートする
っていう提案が一番上に出てきたので、import文を追加しました。import com.sun.glass.events.WindowEvent;これが元凶でした。
次に表示されたエラーが
型 new WindowAdapter(){} からのメソッド windowClosing(WindowEvent) はローカルで使用されません
Eclipseの言われた通りに修正したのにあかんやん。どゆこと?
上のエラーを簡単に言ったら
メソッドの使い方がちゃうよ
ってことだけど、、、なんでEclipseの言った通りにやったのにさ、解決してへんやんって感じでした。そこから次のようなコメントを挿入することが提案される
@SuppressWarnings("unused")いや、これって、使ってないよって話じゃん。。。
んで教材をみて、サンプルと比較しても該当する部分は完全に一致しているように見える。サンプルの該当部分をコピペして貼り付けても同じエラーが起こるので、スペルミスや、見えないコードが混じり込んでいることも考えられないし。。。。
というのでどんどん沼にハマって行きました。。。。
同じ名前のクラスが異なるパッケージに複数ある
また最初のエラーが出ている状態のコードに戻して、Eclipseに表示される提案部分を確認したら、スクロールして下の方にも別の提案がありました。
そこに
java.awt.Eventをインポートする
っていうような提案がありました。ひょっとしてそれじゃねぇのか?って思ったらビンゴでした。
経験が浅いと
Eclipseの一番上に出てくる提案部分を採用して修正すればいいんじゃないか
と思っていたのが間違い。だめ絶対。結局インポート文が間違っていた
import java.awt.Event;とすれば一瞬で解決しました。
じゃあcom.sun.glass.WindowEventはなんなの?
ここにありました。違うパッケージにあるんですね。ややこしい。
結局これでめっちゃ時間がかかってしまった。。。。
2時間くらいかかったけど実りはあったかなと
エラーの解決手順が身についた?
これは自分で思っていることですけど、
- Eclipseに頼りっきりにしない
- 公式ドキュメントを確認する
- ググってみて英語のページしかでてこないけど、ちゃんと読む
- JREライブラリを確認するようになった
- スペルミス以外のミスの経験が得られた
- 自分で解決した経験が得られた
たかだかインポート文が間違っていたというしょうもないミスだったのに2時間くらいかかってしまったのはアホらしいですけど、こういう経験が身になるのかなと信じています。
基本的にはエラーは自分で解決できていますが、やっぱり同様のエラーが出てきた時には教えてもらったことよりも解決するのが断然早い。
そして、なんとなくエラー解決の手順が自分なりに形になってくるので、新しいエラーに関しても手順自体は生かせるかなと感じました。
- 投稿日:2020-10-17T12:23:10+09:00
Dagger Hilt (DevFest 2020 資料)
DevFestの資料の記事版です。ステップごとにサンプルアプリの差分付きで説明します。
なぜDependency Injectionが必要なのか から始め、Dagger Hiltの説明、実践的なプラクティスまで説明していきます!Dependency Injection(DI)とはなにか
なぜDIが必要なのか
DI、ちょっと難しいイメージありますが、そもそもなんで必要なんでしょうか?
作っているのが動画再生するアプリでVideoPlayerというクラスがあるとしましょう。
VideoPlayerのクラスの中にデータベースやcodecなどがハードコードされています。class VideoPlayer { // データベースにビデオの一覧が保存されている (Roomというライブラリを使っている) private val database = Room .databaseBuilder( App.instance, VideoDatabase::class.java, "database" ) .createFromAsset("videos.db") .build() // 使えるコーデック一覧 private val codecs = listOf(FMP4, WebM, MPEG_TS, AV1) private var isPlaying = false fun play() { ... } }使うときにはこれだけで、十分シンプルに見えます。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val videoPlayer = VideoPlayer() videoPlayer.play() } }しかし、これだけだと開発を進めていく中で以下のような問題が起こります。
- VideoPlayerを再度使いたい場合、データベースやコーデックがハードコードされているので、データベースを変えて再生したりなどができない。
- 実行時やデバッグで交換できない。
- テストでも交換できない。例えばテストではメモリ内のDB使いたい場合などは困る。
- さまざまな依存関係がコードの中に入ってくるのでコードが見にくくなり、リファクタリングを難しくする。
この問題を簡単に回避するために簡単なDIを試してみましょう!
簡単にDIしてみよう
ここでDependency Injection(DI)です。日本語だと依存性の注入です。
最初は簡単にコンストラクタインジェクションというコンストラクタを使って依存性を注入する方法を紹介します。
簡単にVideoPlayerのコンストラクタに依存関係を渡してあげるだけです。
他にもsetterによって注入する方法をsetter injectionといいます。コンストラクタインジェクションの例
差分: https://github.com/takahirom/hilt-sample-app/commit/a1fdef28515d158577313b90f7c2590bd5905366VideoPlayerは依存関係が交換可能で、シンプルになった!
class VideoPlayer( private val database: VideoDatabase, private val codecs: List<Codec> ) { private var isPlaying = false fun play() { ... } }コンストラクタインジェクションを使うと使う側で、そのクラスの依存関係を先に作ってからでないとインスタンス化することができません。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val videoDatabase = Room .databaseBuilder( this, VideoDatabase::class.java, "database" ) .createFromAsset("videos.db") .build() val codecs = listOf(FMP4, WebM, MPEG_TS, AV1) val videoPlayer = VideoPlayer(videoDatabase, codecs) videoPlayer.play() } }いろんな画面で、VideoPlayerを作るたびにこのようなコードを書く必要があるので、これは ボイラープレートとなりえます。
この処理をよく見ると"VideoPlayerを作るための処理"と"VideoPlayer#playを呼び出す処理"があることが分かります。VideoPlayerを作るための処理
"VideoPlayerを作るための処理"は、ただ、他の型やクラスを作るためのロジックで、これを"コンストラクションロジック"と呼びます。
val videoDatabase = Room .databaseBuilder( this, VideoDatabase::class.java, "database" ) .createFromAsset("videos.db") .build() val codecs = listOf(FMP4, WebM, MPEG_TS, AV1) val videoPlayer = VideoPlayer(videoDatabase, codecs)VideoPlayer#playを呼び出す処理
"VideoPlayer#playを呼び出す処理"は、アプリの価値を作るためのロジックとなり、"ビジネスロジック"と呼びます。ここではコンストラクションロジック以外をこう呼ばせてください。
videoPlayer.play()コンストラクションロジックとビジネスロジックが一緒になっていると、コードを追ったり、読んだりするのを難しくします。
またそれはクラスを読む人にとってあまり意味のないものであることが多いです。
Dependency Injection(DI)を行うライブラリでは、このコンストラクションロジックとビジネスロジックを分離することができます。DIについてまとめ
- さまざまな良い影響があるのでDIを行おう!
- インスタンス化のためのボイラープレートを避けるためにDIライブラリを使おう!
AndroidではDIが難しい
フレームワークがActivityなどのインスタンスを作ってしまいます。例えば、startActivityなどするとインスタンスが作られてしまいます。(コンストラクタをいじれません。)
// Androidのフレームワークによって勝手にActivityがインスタンス化される startActivity(Intent(context, MainActivity::class))AndroidのAPI Level 28からFactoryでActivityを作れるようになるなど、改善はされていますが、Android 9以上でないと動作しないので、現状では現実的ではないです。
Daggerがこれまでの解決策でした。
上位1万のアプリで74%がDagger使っており、今の解決策としてはこれがメインです。
しかし、アンケートによると49%のユーザーがより良いDIの解決策を必要としていたようです。どんなDIの解決策が必要だったか?
- Opinionated(意見を持った。)
決めるのを楽にして、使うのを楽にする (後述)- セットアップが簡単
(DaggerはAndroidではなく、Java用だったため、Daggerは設定が難しかった)- 重要な部分にフォーカスできる
これがDagger Hiltが生まれたメインの理由のようです。
Dagger Hilt
Dagger HiltはDaggerの上に構築されたライブラリです。
Daggerの良いところを使うことができます。
GoogleのAndroidXチームとDaggerチームで共同で作られています。
- AndroidにおけるDIの標準化
どのようにAndroidでDIするのかを標準化(standardize)します。- Dagger上に構築
Daggerのためのコードを生成します。- アノテーションベース
Hiltに対して、アノテーションで何をしたいのかを伝えます。- ツールサポート
Android Studio 4.1以降では左側にガターアイコンが表示される。例えば依存関係がどこから来たのかがわかります。
- AndroidX Extension
ViewModelとWorkManagerで使える。他のライブラリも追加予定です。Dagger HiltをVideoPlayerの例で使ってみよう
どうにかして、Dagger HiltにVideoPlayerの作り方を教えないといけないですが、どのように教えたら良いでしょうか?
一番基本的なDagger Hiltへのインスタンスの作り方の教え方は、コンストラクタに@Inject
をつけることでです。
現状はVideoPlayerに依存関係をもたせていません。
Dagger Hiltはこのクラスをただインスタンス化するだけなので、作ることができます。class VideoPlayer @Inject constructor() { private var isPlaying = false fun play() { ... } }次にこのアプリはHiltで動くということをHiltに教えないといけません。
AndroidのApplicationクラスで@HiltAndroidApp
を使うと、このアプリがHiltで動くということを教えられます。また、@HiltAndroidApp
を使うと内部的にはComponent
が作られます。このComponent
とは、コンストラクションロジックと、作ったインスタンスを保持する部分です。(Componentについては後でもう一度触れます。)@HiltAndroidApp class VideoApp : Application()Activtyのコンストラクタがいじれないということでしたが、その対応としてActivityに
@AndroidEntryPoint
をつけます。@AndroidEntryPoint class MainActivity : AppCompatActivity() {
@AndroidEntryPoint
をつけることで、3つのことをHiltに対して教えます。
- "このActivityはInjectionを行う。このActivityはDependency Injectionを使う"
- "ActivityのComponentをこのActivityに追加。"
- "HiltからDependencyを持ってくる"
変数に
@Inject
アノテーションを付けています。
これはHiltからInjectされることを意味します。"Actiivtyが作られたときに、VideoPlayerをInjectして" とHiltに教えている形です。
そして、onCreateメソッドではVideoPlayerを呼ぶなど好きなことができます。@AndroidEntryPoint class MainActivity : AppCompatActivity() { // ↓ Dagger HiltによってonCreateでInjectされる @Inject lateinit var videoPlayer: VideoPlayer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) videoPlayer.play() } }魔法?どこでフィールドに代入されるの?
少しだけ中身の仕組みを知っておいたほうが分かりやすいと思うので、説明しておきます。
@AndroidEntryPoint
がついているActivityはHiltによって変換されます。
Hiltによって変換後にMainActivityとAppCompatActivityの間に
生成されたHilt_MainActivityが入ります。
Hilt_MainActivityのonCreateの中でフィールドにInjectされます。Hiltによって変換後のコード
@AndroidEntryPoint class MainActivity : Hilt_MainActivity() { // Hilt_MainActivityになっている @Inject lateinit var videoPlayer: VideoPlayer override fun onCreate(savedInstanceState: Bundle?) { // この中でフィールドにInjectされる super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) videoPlayer.play() } }VideoPlayerにVideoDatabaseをInjectさせるには?
これで一応MainActivityでVideoPlayerが使えるようになりました。ただ、これではVideoPlayerがDatabaseなどにアクセスできません。どのようにしていけばいいでしょうか?
// ↓ Databaseなどを持っていない class VideoPlayer @Inject constructor() { private var isPlaying = false fun play() { ... } }もし、VideoDatabaseのコンストラクタを変更することができるのであれば、同様にVideoDatabaseに
@Inject
をつけて、Hiltにインスタンスの作り方を教えられます。Daggerが勝手に依存関係のインスタンス、VideoDatabaseを先に作ってからVideoPlayerをインスタンス化してくれます。class VideoPlayer @Inject constructor( private val database: VideoDatabase ) { private var isPlaying = false fun play() { ... } } class VideoDatabase @Inject constructor() { ... }ただ今回はRoomによって作られるインスタンスになるので、コンストラクタをいじって
@Inject
をつけることができません。そのためどうにかしてDagger Hiltに以下のVideoDatabaseの作り方を教えないといけません。val videoDatabase = Room .databaseBuilder( this, VideoDatabase::class.java, "database" ) .createFromAsset("videos.db") .build()そこでModuleを使います。Moduleを使うことで、Hiltにインスタンスの作り方を教えることができます。
@Module
と@InstallIn
というアノテーションを付けるただのクラスです。
そのModuleにはメソッドを追加します。
Moduleにあるメソッドは料理のレシピだと考えると分かりやすいです。VideoDatabaseの型を作るレシピになり、このレシピをHiltに教えています。
このレシピを@InstallIn
でSingletonComponent
に置きます。SingletonComponent
は ApplicationのComponentに追加するということです。メソッドをよく見ると
@Provides
と書いてあることが分かります。HiltにどのようにVideoDatabaseを作るのかを教えるメソッドということを教えています。
HiltがVideoDatabaseを作る必要があるときに、このメソッドを実行してインスタンスを返します。
ちなみに、contextクラスのインスタンスが@ApplicationContext
で提供されていますが、Hiltで事前定義されたクラスもいくつかあり、Hiltがいくつかのインスタンスを提供してくれます。
差分: https://github.com/takahirom/hilt-sample-app/commit/c85a6f668a0bf447c0a4b119f4f6d8cc8c2cff80@Module @InstallIn(SingletonComponent::class) object DataModule { @Provides fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase { return Room .databaseBuilder( context, VideoDatabase::class.java, "database" ) .createFromAsset("videos.db") .build() } }さて、この
SingletonComponent
が出てきましたが、 Componentについてもう少し触れておきましょう。Component
Componentは以下のことができます。
- どのようにオブジェクトをビルドするのかのロジックを持っています。
VideoDatabaseはこうやって作る。。など
VideDatabase : Room.databaseBuilder() … .build()インスタンス生成の順序のロジックを持っています。
"VideoDatabaseの後にVideoPlayerを生成する。"などスコープによってインスタンスを使い回します。 (後述)
Dagger Hilt標準のComponent
HiltはOpnionedであると言いましたが、Dagger Hiltには標準のComponentが存在し、Componentの構造について迷わなくて良くなっています。
この図はComponentの階層を表しており、アプリケーション全体のSingletonComponent、画面回転でも生き残るActivityRetainedCompoent、Activityと紐づくActivityComponentなどのComponentがある構造になっています。
上についているアノテーションはスコープアノテーションです。これについては後ほど説明します
例えば、SingletonComponentやConfigration Changeでも生き残るActivityRetainedComponent、ActivityComponent、そしてFragmentComponentなどが付随します。
https://dagger.dev/hilt/components より今回の例ではSingletonComponentにVideoPlayerの作り方とVideoDatabaseの作り方が入っており、その作成順序も入っています。
インスタンスを共有したいときはどうするのか
例えば現状ではVideoDatabaseは現在使われるたびにインスタンス化されてしまいます。
@AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var videoPlayer: VideoPlayer @Inject lateinit var videoPlayer2: VideoPlayer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) println(videoPlayer.database) // VideoDatabase_Impl@764b474 ← ハッシュコードが違う println(videoPlayer2.database) // VideoDatabase_Impl@a945d9d ← ハッシュコードが違う } } class VideoPlayer @Inject constructor( val database: VideoDatabase ) @Module @InstallIn(SingletonComponent::class) object DataModule { @Provides fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase { return ... } }コネクションを使いまわしたいなど様々な理由で、インスタンスを使いまわしたいことがあります。また今回の例とは関係ないですが、Androidの通信で一般的に使われるOkHttpはインスタンスをアプリ全体で使いまわすとパフォーマンスが良くなります。 (https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/ より)
Activity内で共有したい場合は、
@ActivityScoped
を使うことでActivityComponentで保持されるため、
Activity内で同じインスタンスを使い回せます。この@ActivityScoped
をScope AnnotationといいますこれでActivity内でインスタンスを共有できます。
差分: https://github.com/takahirom/hilt-sample-app/commit/f895dfac123a0317b9e0e247af3a48b57388ad5d@Module @InstallIn(ActivityComponent::class) object DataModule { @ActivityScoped @Provides fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase { ... } }
@ActivityScoped
ではActivityが違えば別のインスタンスになってしまいます。アプリ全体で使いまわしたいときは@Singleton
のScope Annotationを使うことでSingletonComponentで保持されるため、アプリ全体で使えます。
差分: https://github.com/takahirom/hilt-sample-app/commit/64cc1b50388cf9c79fba26a774ae86efb1f093bc@Module @InstallIn(SingletonComponent::class) object DataModule { @Singleton @Provides fun provideVideoDB(@ApplicationContext context: Context): VideoDatabase { ... } }VideoDtabaseはSingletonComponent内でインスタンスも保持されるようになりました。
Hiltが管理していないクラスから、Hiltの依存関係を使いたい時
Hiltが管理しているMainActivityやVideoPlayerクラスではHiltから依存関係を取得できますが、Hiltが管理していないクラスでは取得が難しい場合があります。
例えば、ContentProviderクラスや、他のライブラリが生成するクラス、Dagger Hiltにマイグレーションしているときの既存のクラスなどです。
ここで、EntryPointという仕組みが使えます。EntryPointを使うことで、HiltのもつComponentが持つ依存関係にアクセスすることができます。これはHiltで管理できていないActivityでHiltがコンストラクションロジックをもつVideoPlayerを使う例となります。
差分: https://github.com/takahirom/hilt-sample-app/commit/d66fb46b395b0c9b6a98ff91bd55f3c4f12c99c9class NonHiltActivity : AppCompatActivity() { @EntryPoint // @EntryPointをつける。 @InstallIn(SingletonComponent::class) interface NonHiltActivityEntryPoint { fun videoPlayer(): VideoPlayer } override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { super.onCreate(savedInstanceState, persistentState) val entryPoint = EntryPointAccessors.fromApplication( applicationContext, NonHiltActivityEntryPoint::class.java ) val videoPlayer = entryPoint.videoPlayer() videoPlayer.play() } }テスト
一般的なテストの書き方と同じように自分で対象のオブジェクトnewしてテストを書くことができます。
この場合、自分で依存関係を先に作らなくてはいけません。
差分: https://github.com/takahirom/hilt-sample-app/commit/068082bf7bcb20ecbb1258ac6a3027988d624303@Test fun normalTest() { // メモリ内にDBを作る val database = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), VideoDatabase::class.java ).build() val videoPlayer = VideoPlayer(database) videoPlayer.play() assertThat(videoPlayer.isPlaying, `is`(true)) }Hiltを使うと、このように自分で依存関係を作らずにHiltにインスタンスを作らせることができます。
しかし、今回は実際のDatabaseを使わずにメモリ内にDatabaseを使ってそれを使いたいです。どのようにするでしょうか?@HiltAndroidTest class VideoPlayerTest { @get:Rule var hiltAndroidRule = HiltAndroidRule(this) @Inject lateinit var videoPlayer: VideoPlayer @Test fun play() { hiltAndroidRule.inject() videoPlayer.play() assertThat(videoPlayer.isPlaying, `is`(true)) }ここで
@UninstallModules(DataModule::class)
することで、Dagger HiltからDataModuleが持つVideoDatabaseの作り方を忘れさせることができます。
そして、テストの中で、新たなModuleを定義することで、Databaseを提供することができます。 テストの外でModuleを宣言するとテスト全体でInstallされます。
差分: https://github.com/takahirom/hilt-sample-app/commit/4c862ee62e8dfc133ea6e7e3ff0735c0497cfb6a@HiltAndroidTest @UninstallModules(DataModule::class) @RunWith(RobolectricTestRunner::class) class VideoPlayerTest { @InstallIn(SingletonComponent::class) @Module class TestDataModule { @Provides fun provideVideoDatabase(): VideoDatabase { return Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), VideoDatabase::class.java ).build() } }Daggerからの Dagger Hiltマイグレーションについて
Daggerを知っている人向けの話になりますので、Dagger分からんという方は、へーっていうぐらいで見てください。
DaggerからDagger Hiltに一歩ずつマイグレーションしていく方法について触れておきます。
Hiltへの導入準備
Daggerのライブラリをバージョンアップしておく
→ 普通にマイグレーションするだけです。
Daggerのコンポーネントの状況を見てみる
Daggerのコンポーネントの図を出すツールは昔からいろんなプラグインがあります。
Dagger SPI(Service provider interface)というDagger内部の
情報を取り出せるDaggerのAPIのようなものがあるので、それを使っているツールで確認することをおすすめします。https://github.com/arunkumar9t2/scabbard
https://github.com/Snapchat/dagger-browser
などそしてある程度、どのコンポーネントが、どのコンポーネントに対応付けられそうかを見てみます。
scabbardを使った図の例
今回の状況の前提
例とする状況はApplicationレベルのコンポーネントのAppComponentがあり、その下にActivityごとのComponentがたくさんあるような形になっているとします。
(Daggerだとコンポーネントの形が標準化されていないので、そもそものComponentの形もさまざまになります。)AppComponentの作り方でModuleを引数を渡す形で作っている場合はそれをやめる
Dagger Hiltはこのモジュールを渡し方して作る作り方をサポートしていないためやめる必要があります。Moduleの引数をなくして参照してあげることで可能です。
Daggerのドキュメント的にもそれは良くないらしいです。(don't do thisって書いてあります https://dagger.dev/dev-guide/testing )NG
DaggerAppComponent .builder() .networkModule(networkModule) .build()OK
DaggerAppComponent.factory() .create(application)Hiltを導入してAppComponentを置き換える
ApplicationレベルのComponentをDagger HiltのSingletonComponentに置き換える
Dagger Hiltのライブラリを導入します。これは基本的なドキュメントをご確認ください。
https://dagger.dev/hilt/migration-guide少しずつマイグレーションしていく場合は
disableModulesHaveInstallInCheck
を入れる必要があります。Dagger Hiltで標準だと
@InstallIn
が入っていない既存のモジュールがあるときにエラーになってしまいます。このオプションを入れるとそのエラーを出さなくして、ただライブラリを入れるだけということができます。javaCompileOptions { annotationProcessorOptions { // ↓ **ここが+=になっていないとDagger Hiltのプラグインがargumentを追加するのでハマるので注意** arguments += [ "dagger.hilt.disableModulesHaveInstallInCheck": "true" ] } }Dagger HiltのEntryPointはComponentから依存関係をとってくるだけでなく、サブコンポーネントを作ることもできるので、その機能を使って置き換えます。 (ここは説明飛ばします。)
差分: https://github.com/takahirom/hilt-sample-app/commit/8e542f191bb50ce50db30cb2a72a569f7d17b178@Subcomponent interface JustDaggerComponent { @Subcomponent.Factory interface Factory { fun create(): JustDaggerComponent } fun inject(justDaggerActivity: JustDaggerActivity) } @InstallIn(SingletonComponent::class) @EntryPoint interface JustDaggerEntryPoint { fun activityComponentFactory(): JustDaggerComponent.Factory } class JustDaggerActivity : AppCompatActivity() { @Inject lateinit var videoPlayer: VideoPlayer override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { super.onCreate(savedInstanceState, persistentState) // old: appComponent.justDaggerComponent().inject(this) val entryPoint = EntryPointAccessors.fromApplication( applicationContext, JustDaggerEntryPoint::class.java ) entryPoint.activityComponentFactory().create().inject(this) videoPlayer.play() } }既存のActivityのComponentをHiltに置き換えていく
基本的に
@AndroidEntryPoint
をつけて、既存のDaggerの処理を外していくということになります。
JustDaggerActivity
の形をMainActivity
の形に変えていきます。マイグレーションについては、他にもいろいろありますが、コードラボが詳しいので考えている方はCodelabをやってみてください。
Dagger HiltとJetpackの連携
よく開発で利用されるViewModelやWorkManagerといったJetpackのComponentと連携するライブラリが提供されていて、それを利用することができます。
差分: https://github.com/takahirom/hilt-sample-app/commit/1bec3370fec0fd5b4233db1884e8427bcf91a540
Androidアプリの開発ではViewModelをよく使います。まずはViewModelについて見ていきましょう。
ViewModelではコンストラクタに変更が加えられますが、通常、ProviderやFactoryなどを経由して作成するため難しいですが、この部分をDagger Hiltはうまく隠蔽して簡単にViewModelを作ってくれます。@AndroidEntryPoint class MainActivity : AppCompatActivity() { private val videoPlayerViewModel: VideoPlayerViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) videoPlayerViewModel.play() } } class VideoPlayerViewModel @ViewModelInject constructor( private val videoPlayer: VideoPlayer ) : ViewModel() { fun play() { videoPlayer.play() } }Dagger Hilt 実践プラクティス
Google(or Googler)のサンプルを参照する
迷ったら、サンプルアプリ達を見てみましょう。
Architecture Samples
https://github.com/android/architecture-samples/tree/dev-hilt
Goole I/Oアプリ
https://github.com/google/iosched
Sunflower
https://github.com/android/sunflower
chrisbanes/tivi
https://github.com/chrisbanes/tivifastInitモードが有効になるので、影響を確認する
Dagger HiltはComponentの形が標準化されたことによって、たくさんのSingletonComponentなどのコンポーネントに、この型はこう作るなどのバインディングが入るようになります。
通常、Daggerではこのバインディングの数が増えると増えるだけインスタンス化に時間がかかります。Dagger Hiltを入れたタイミングで通常のモードではなくfastInitモードが有効になることで、これが時間がかからなくなります。しかし、この処理にはトレードオフもあるようなので、リリース後にFirebase PerformanceやAndroid Vitalsなどで確認してみましょう。val PROCESSOR_OPTIONS = listOf( "dagger.fastInit" to "enabled",
生成されるコードの比較
fastInitがdisabledになっている場合public final class DaggerApp_HiltComponents_SingletonC extends App_HiltComponents.SingletonC { private Provider<Context> provideContextProvider; private Provider<VideoDatabase> provideVideoDBProvider; private DaggerApp_HiltComponents_SingletonC( ApplicationContextModule applicationContextModuleParam) { initialize(applicationContextModuleParam); } ... @SuppressWarnings("unchecked") private void initialize(final ApplicationContextModule applicationContextModuleParam) { this.provideContextProvider = ApplicationContextModule_ProvideContextFactory.create(applicationContextModuleParam); this.provideVideoDBProvider = DoubleCheck.provider(DataModule_ProvideVideoDBFactory.create(provideContextProvider)); } @Override public VideoPlayer videoPlayer() { return new VideoPlayer(provideVideoDBProvider.get()); }fastInitがenabled
Providerが値を保持する代わりにComponentが値を保持するようになる。public final class DaggerApp_HiltComponents_SingletonC extends App_HiltComponents.SingletonC { private final ApplicationContextModule applicationContextModule; private volatile Object videoDatabase = new MemoizedSentinel(); private DaggerApp_HiltComponents_SingletonC( ApplicationContextModule applicationContextModuleParam) { this.applicationContextModule = applicationContextModuleParam; } private VideoDatabase videoDatabase() { Object local = videoDatabase; if (local instanceof MemoizedSentinel) { synchronized (local) { local = videoDatabase; if (local instanceof MemoizedSentinel) { local = DataModule_ProvideVideoDBFactory.provideVideoDB(ApplicationContextModule_ProvideContextFactory.provideContext(applicationContextModule)); videoDatabase = DoubleCheck.reentrantCheck(videoDatabase, local); } } } return (VideoDatabase) local; } @Override public VideoPlayer videoPlayer() { return new VideoPlayer(videoDatabase()); }
詳細画面などでIDを渡していきたい場合はどうするのか?
Dagger HiltではComponentの構造が標準化されているため、例えばEpisodeDetailComponentを作って、そこで画面詳細IDを配布というようなことは難しいです。
これに対してさまざまなやり方が考えられますが、Googleのサンプルでのやり方は一つのようです。
Daggerを使って配布せず、直接渡す方法です。
HiltのComponentを作るための公式のページに、バックグラウンドタスクの話で少し文脈は違うのですが、"だいたいは自分で渡したほうがシンプルで十分"という話が出てきます。
一番渡しがちなAssisted InjectについてはただViewModelに渡すときは少しだけ工夫することができます。https://dagger.dev/hilt/custom-components
for most background tasks, a component really isn’t necessary and only adds complexity where simply passing a couple objects on the call stack is simpler and sufficient.
コールスタックで引数を渡すだけのほうがシンプルで十分で、Componentは複雑さを増すだけ。ということを言っています。
Architecture Samples
https://github.com/android/architecture-samples/blob/dev-hilt/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt#L90
Iosched
https://github.com/google/iosched/blob/b428d2be4bb96bd423e47cb709c906ce5d02150f/mobile/src/main/java/com/google/samples/apps/iosched/ui/speaker/SpeakerViewModel.kt#L101
Sunflower
https://github.com/android/sunflower/blob/2bbe628f3eb697091567c3be8f756cfb7eb7258a/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt#L55
chrisbanes/tivi
https://github.com/chrisbanes/tivi/blob/27348c6e4705c707ceaa1edc1a3080efa06109ae/ui-showdetails/src/main/java/app/tivi/showdetails/details/ShowDetailsFragment.kt#L60ViewModelにコンストラクタで値を渡す
“IDなどを直接渡していく”ということでしたが、ViewModelのコンストラクタで渡せないと、lateinitにしたり、nullableにして少し安全じゃない形になっちゃいますよね?
AssistedInjectというライブラリで、コンストラクタ引数を渡す実装ができる。(今後Daggerにも組み込まれるそうです)差分: https://github.com/takahirom/hilt-sample-app/commit/6584808f8fe13cc92317df50d413f828d1dfdf00
DaggerはAssistedInjectと呼ばれるものを対応しようとしています。これはInjectするときに、プログラムから引数で値を渡せるというものです。
https://github.com/google/dagger/issues/1825これを先に使えるライブラリがあります。
https://github.com/square/AssistedInject具体的にはGooglerの以下のgistの内容を使ってViewModelにコンストラクタで値を渡すことができます。
https://gist.github.com/manuelvicnt/437668cda3a891d347e134b1de29aee1本質的に理解しようとすると大変なので、仕組みが気になる方は以下を読んでみてください。
https://qiita.com/takahirom/items/f28ceb7a6d4e69e4dafeEntryPointの定義場所
EntryPointは、基本的にはGoogleのサンプルでは使われていないようです。
大きいアプリのマイグレーションなどでは使われると思われるので、紹介しておきます。
EntryPointはどこにでも書くことができますが、どこに書くのがいいでしょうか?
依存関係を取得するときに使うEntryPointは、使う場合には必要な依存関係のみを取得したほうが依存するオブジェクトを少なくできるので、基本的には取得する場所に定義して利用していきましょう。class NonHiltActivity : AppCompatActivity() { @EntryPoint @InstallIn(SingletonComponent::class) interface NonHiltActivityEntryPoint { fun videoPlayer(): VideoPlayer } override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { super.onCreate(savedInstanceState, persistentState) val entryPoint = EntryPointAccessors.fromApplication( applicationContext, NonHiltActivityEntryPoint::class.java ) val videoPlayer = entryPoint.videoPlayer() videoPlayer.play() } }マルチモジュール
まだちゃんとしたベストプラクティスがあるわけではないと思いますが、Dagger Hiltとマルチモジュールについて考えておきます。以下のようなモジュール構成があったとします。
Applicationクラスをコンパイルする Gradleモジュールは、
すべての Hilt モジュールおよびコンストラクタ インジェクションで注入するクラスを
推移的依存関係に含める必要があります。https://developer.android.com/training/dependency-injection/hilt-multi-module?hl=ja より
ということなので、以下のようにルートのモジュールからDaggerのModuleを持つGradleモジュールへの参照が必要になります。
この部分に関して、真ん中の形が色んなパターンで無駄に依存関係を増やさずに動くので、いいのかなとは思うのですが、まだベストプラクティスと言えるものはないです。
ただ、モジュールを作ったときにクラスパスに含めるだけで@InstallIn
されたModuleがコンポーネントにインストールされて使えるので、すごく楽で、本当に使いやすいです。参考
Googleの方のchrisbanes/tiviのアプリのappモジュールからの参照
ルートのGradleモジュールから各モジュールに参照していることが分かる。
Hiltでビルドできる?できない?みたいな実験する環境作るのめんどうですよね?Hiltが導入済みのアプリで実験したくなったらこのサンプルプロジェクトを用意しているので、実験してみてください。
https://github.com/takahirom/dagger-transitive-playground/tree/hiltテストのプラクティス
実際の依存関係を使おう
Hilt Testing Philosophyというページが有り、Dagger Hiltを使ってテストをする際のプラクティスが書いてあります。これがかなり主張を含んだもので面白いのでぜひ読んでみてください。
https://dagger.dev/hilt/testing-philosophy
こちらに自分のメモがあります。
https://qiita.com/takahirom/items/a3e406b067ad645605daこれによると2つ言いたいことがあるようです。
- ユーザーからの観点でテストする
ユーザーとは実際のユーザーもクラスのユーザーも、APIのユーザーも含む。internalなメソッド名や実装になど依存せず、テストが壊れることがユーザーの観点から変更があったことを意味する- 実際の依存関係を利用する
なぜ実際の依存関係を使うか?
- 実際の依存関係は本当の問題を捕捉しやすい。モックのように古いまま残されたりしない。
- "ユーザーからの観点でテスト"と組み合わせることで、同じカバレッジでもっと少ないテストの量で書くことができる。
- テストが壊れることが、FakeやMockの設定ミスによる問題による問題の代わりに、実際の問題を指し示す (そして、逆に言えばテストがパスすることはコードがちゃんと動くことを意味する)
- "ユーザーからの観点でテスト"と"実際の依存関係を使う"は相性が良い。依存関係を入れ替えないため。
どうやってDagger Hiltで実際の依存関係を使うか?
普通にテストして実際の依存関係を使おうとすると依存関係を作るためのボイラープレートが発生します。
PlayerFragmentを作るためにViewModelのFactoryが必要で、ViewModelを作るにはVideoPlayerが必要で、VideoPlayerを作るにはVideoDatabaseが必要になるなどを書いていくと。。大変になります。launchFragment { // たいへん! PlayerFragment().apply { videoPlayerViewModelAssistedFactory = object : VideoPlayerViewModel.AssistedFactory { override fun create(videoId: String): VideoPlayerViewModel { return VideoPlayerViewModel( videoPlayer = VideoPlayer( database = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), VideoDatabase::class.java ).build() ), videoId = "video_id" ) } } } } onView(withText("playing")).check(matches(isDisplayed()))紹介したように、Dagger Hiltを使ってテストでもInjectを行うことで実際の依存関係を使ってテストをすることが可能になります。
@RunWith(AndroidJUnit4::class) @HiltAndroidTest @UninstallModules(DataModule::class) class AndroidPlayerFragmentTest { @InstallIn(SingletonComponent::class) @Module class TestDataModule { @Provides fun provideVideoDatabase(): VideoDatabase { return Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), VideoDatabase::class.java ).build() } } @get:Rule var hiltAndroidRule = HiltAndroidRule(this) @Test fun play() { hiltAndroidRule.inject() launchFragmentInHiltContainer<PlayerFragment> { } onView(withText("playing")).check(matches(isDisplayed())) }さまざまなデメリットがあるのでTestでのCustom Applicationクラスはやめよう
一応
@CustomTestApplication
でカスタムアプリケーションを使ってテストできます。
テスト用のTestAppクラスなどを作っているのであればHiltと組み合わせると
さまざまなデメリットが発生します。また一般的にやめておいたほうが良さそうです。
- テストでは以下の問題があるため、:カスタムアプリケーションでは@Injectのフィールドを使えない*。(コンパイルエラー)
- Applicationはテストをまたいで生存してしまうので、テストをまたいだ状態のリークを起こす。
- 子が親に依存するテストになってしまう ので、テストの独立性を高めるためにやめておくべき。など
Dagger Hiltの細かいTips
変換されたコードのAndroid Studioでの実行が現状、サポートされていないため、Gradleでテストを実行する必要がある。(Android StudioでGradleの設定を行ってAndroid Studioで実行させることもできる。)
詳しくは: https://dagger.dev/hilt/gradle-setup.html#running-with-android-studio
Android Studio 4.1からRun/Debug Configrationsが保存できるようになったので、活用できます。
差分: https://github.com/takahirom/hilt-sample-app/commit/2274ff3b5712e6b266cf022ff91f4581532bf45bテストを楽にするルールを作れる
HiltAndroidAutoInjectRuleなどを用意しておくと自分でInjectを呼ばなくても動くようになります。
@get:Rule val hiltAndroidAutoInjectRule = HiltAndroidAutoInjectRule(this) class HiltAndroidAutoInjectRule(testInstance: Any) : TestRule { private val hiltAndroidRule = HiltAndroidRule(testInstance) private val delegate = RuleChain .outerRule(hiltAndroidRule) .around(HiltInjectRule(hiltAndroidRule)) override fun apply(base: Statement?, description: Description?): Statement { return delegate.apply(base, description) } } class HiltInjectRule(val rule: HiltAndroidRule) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) rule.inject() } }まとめ
以下について話してきました。
- なぜDIを使うのか
- なぜDagger Hiltなのか
- 基本的な使い方や概念
- テスト
- マイグレーション
- 実践プラクティス
- Tips
Dagger Hiltを使うことでさまざまないい効果が見込めるので、Dagger Hiltを使ってアプリを作ってみましょう!
参考
公式ウェブページ
https://dagger.dev/hilt/
https://developer.android.com/training/dependency-injection/hilt-android?hl=jaAndroid Dependency Injection
https://www.youtube.com/watch?v=B56oV3IHMxg
Dagger Hilt Deep Dive
https://www.youtube.com/watch?v=4di2TTqeCrEArchitecture Samples
https://github.com/android/architecture-samples/tree/dev-hilt
Goole I/Oアプリ
https://github.com/google/iosched
Sunflower
https://github.com/android/sunflower
chrisbanes/tivi
https://github.com/chrisbanes/tivi