Gradle4 + Jersey2 + Kotlin でRESTサービスを作ってみる
SPA でアプリ作ろう思って、サーバに当初 Spring 考えてたけど、こっちの方が API サーバが楽そうだった。
で、Kotlin は個人的な好み
教科書はこれ
JAX-RS(Jersey)+GradleでWebアプリを作る - Olivinecafe - blog
Gradle
汎用ビルドツール。
Java 専用ではないので、Java 書きたければ基本はプラグイン。
独自の DSL で記述するが、Java ライブラリとか普通に叩けた。
個人的に好きなのは、gradle wrapper
コマンドで、これを実行するとカレントディレクトリに gradlew
コマンドを吐き出す。
こいつは単独実行可能な gradle
コマンドなので、いちいち使う人に Gradle のインストールを強要しない。
sbt も大概 jar 一つでやってのけるのだが、こういう仕様がないと、複数人での開発がだるくて仕方ない。
とりあえず、build.gradle
はこんな感じで書いた。
apply plugin: 'maven' apply plugin: 'war' apply plugin: 'org.akhikhl.gretty' apply plugin: 'kotlin' def jerseyVersion = '2.25.1' sourceCompatibility = 1.7 buildscript { repositories { jcenter() mavenCentral() } dependencies { classpath 'org.akhikhl.gretty:gretty:+' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.3' } } ext { project.version = '0.0.1' project.group = 'net.white-azalea' appName = 'example' project.description = 'example gradle application/' } configurations { all*.exclude module: 'servlet-api' } dependencies { providedCompile 'javax.ws.rs:javax.ws.rs-api:2.0.1' compile "org.glassfish.jersey.containers:jersey-container-servlet:${jerseyVersion}" testCompile 'junit:junit:4.+' } repositories { jcenter() mavenCentral() mavenLocal() }
とてもざっくり説明すると、
- リリース時は war でビルドするよ
- gretty プラグインを使って、デバック実行時に jetty サーバ使うよ
- Kotlin 言語でやるよ
しかも Java プラグイン入れてないので、Java 書けないよ servlet-api
は war には含まない様にしてるよ
これが含まれてると、Tomcat とかのサーバにデプロイした時バージョン衝突とかめんどくさいことになる場合がある- Jersey2.25.1 を必要とするよ
これを gradlew appRun
すると、依存する jar とか、実行サーバとか諸々勝手にダウンロードしてくる。
開発マシンにわざわざ手動で Tomcat 入れさせる環境とか、ネットワーク接続禁止環境だけでしょ…Web アプリ作るのにネットワーク禁止とかこれいかに?
jersey2
JAX-RS 実装らしいがなんのことはない。
アノテーションでパス指定すれば色々できる。
まずは、src/main/webapp/WEB-INF/web.xml
を記述する。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>jersey-example</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>jersey.config.server.provider.packages</param-name> <param-value>net.white_azalea</param-value> </init-param> <init-param> <param-name>jersey.config.server.provider.scanning.recursive</param-name> <param-value>true</param-value> </init-param> <load-on-startup>10</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jersey-example</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
設定は全て Gradle にお任せモードする web.xml だ。
お次はソース。src/main/kotlin/net.white-azalea/Example.kt
を作成して以下を記述
package net.white_azalea import javax.ws.rs.GET import javax.ws.rs.Path import javax.ws.rs.Produces import javax.ws.rs.core.MediaType @Path("/hello") class Example { @GET @Produces(MediaType.TEXT_HTML) fun hello(): String = "Hello!" }
ここまできたら全部おしまい。
./gradlew appRun
して、http://localhost:8080/jersey-example/hello
にアクセスできれば OK.
jar 依存がだいぶ足りてないので、オブジェクト返すとコケる。
まぁその辺は この辺 みて自力解決しよう。
Redmine3 に Backlogs を入れるメモ
先日入れた Redmine 3.3 にBacklogs を突っ込んでみる。
教科書はこれ。
確かに Gem 競合を除いてしまえば入るっちゃ入る。
[root@localhost redmine]# RAILS_ENV=production bundle exec rake redmine:backlogs:install 3.3.4.stable.16875. You are running backlogs v1.0.6, latest version is 1.0.6 ===================================================== Redmine Backlogs Installer ===================================================== Installing to the production environment. Fetching card labels from http://git.gnome.org...done! Configuring story and task trackers... ----------------------------------------------------- Which trackers do you want to use for your stories? 1. ストーリー 2. バグ 3. 機能 4. サポート Separate values with a space (e.g. 1 3): 1 You selected the following trackers: ストーリー. Is this correct? (y/n) y ----------------------------------------------------- Which tracker do you want to use for your tasks? 1. バグ 2. 機能 3. サポート Choose one from above (or choose none to create a new tracker): 3 You selected サポート. Is this correct? (y/n) y Story and task trackers are now set. Migrating the database...WARNING: 進行中のトランザクションがありません done! Installation complete. Please restart Redmine. Thank you for trying out Redmine Backlogs! [root@localhost redmine]#
CentOS7 に Redmine 3 を試してみるテスト
とりま VirtualBox に CentOS7 を普通にインストールして、ホストオンリーアダプタを有効化。
# nmcli connection modify enp0s3 connection.autoconnect yes
これで普通に ssh 可能となるので、ここからはインストール開始。
Redmine 3.3をCentOS 7.3にインストールする手順 | Redmine.JP Blog
因みに下記のエラーを食った。
----- mysql client is missing. You may need to 'apt-get install libmysqlclient-dev' or 'yum install mysql-devel', and try again. ----- *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options. Provided configuration options: --with-opt-dir --without-opt-dir --with-opt-include=${opt-dir}/include --with-opt-lib=${opt-dir}/lib --with-make-prog --without-make-prog --srcdir=. --curdir --ruby=/usr/local/bin/$(RUBY_BASE_NAME) --with-mysql-dir --without-mysql-dir --with-mysql-include --without-mysql-include=${mysql-dir}/include --with-mysql-lib --without-mysql-lib=${mysql-dir}/lib --with-mysql-config --without-mysql-config --with-mysql-dir --without-mysql-dir --with-mysql-include --without-mysql-include=${mysql-dir}/include --with-mysql-lib --without-mysql-lib=${mysql-dir}/lib --with-mysqlclientlib --without-mysqlclientlib To see why this extension failed to compile, please check the mkmf.log which can be found here: /var/lib/redmine/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0-static/mysql2-0.4.8/mkmf.log extconf failed, exit code 1 Gem files will remain installed in /var/lib/redmine/vendor/bundle/ruby/2.3.0/gems/mysql2-0.4.8 for inspection. Results logged to /var/lib/redmine/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0-static/mysql2-0.4.8/gem_make.out An error occurred while installing mysql2 (0.4.8), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.4.8'` succeeds before bundling. In Gemfile: mysql2
見たまま、mysql client のライブラリをインストールしたら治った。
だがその後も下記に引っかかった。
いったいいくつ引っかかるんだ…
そして、Apache を起動するも、デフォルトページが表示。
/etc/httpd/conf.d/welcome.conf をリネームで退避するも、403 Forbidden.
ログを眺めると
[Mon Jul 24 22:42:58.583973 2017] [autoindex:error] [pid 25600] [client 192.168.56.1:50949] AH01276: Cannot serve directory /var/lib/redmine/public/: No matching DirectoryIndex (index.html) found, and server-generated directory index forbidden by Options directive
だー!
[Redmine Rails] Passenger がシンボリックリンクを解決してくれない | Javable.Jp とか 色々見たけど、なんのことはない、Passenger のインストールでエラー出てただけだった。
やり直したら入る不思議…爆発しる!
まぁとりあえずこれでインストールはできた。
毎度思うがハマりどころが多すぎませんかね?(汗
SPA アプリそろそろ作って見たいんじゃい(3)
とりあえず SpringBootSecurity を設定してみた回
まずは build.gradle に依存を追加する。
dependencirs{ compile('org.springframework.boot:spring-boot-starter-security') // あとはおすきに
そしてログインに使用する DB からデータを取得するためのインターフェースを追加する。
基本的に DB にアカウントとパスワードを格納しているので、それを取得する。
interface AccountRepository : JpaRepository<Account, Long> { fun findOneByName(name: String): Optional<Account> }
次に、Spring がログインの際に使用するユーザ情報のデータ定義を作る。
package net.white_azalea.todo_demo.service.secure import net.white_azalea.todo_demo.repositories.Account import org.springframework.security.core.authority.AuthorityUtils import org.springframework.security.core.userdetails.User class LoginAccount(account: Account) : User(account.name, account.password, AuthorityUtils.createAuthorityList("LOGIN_ROLE")) { }
中身はあってもなくてもいい。
要するにアカウント ID と、ハッシュ化されたパスワードがあり、その時の権限を用意する。
次は Spring がアカウント ID を受け付けたときにログイン情報を検索するロジックの実装。
package net.white_azalea.todo_demo.service.secure import net.white_azalea.todo_demo.repositories.AccountRepository import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Component @Component class AccountDetailService( val accountRepository: AccountRepository ) : UserDetailsService { override fun loadUserByUsername(username: String?): UserDetails { val acc = accountRepository.findOneByName(username.orEmpty()) if (acc.isPresent) { return LoginAccount(acc.get()) } else { throw UsernameNotFoundException("Account does not exists.") } } }
パスワードのエンクリプトと、チェックを行うためのクラスも作成。
package net.white_azalea.todo_demo.service.secure import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Component import java.util.* import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @Component class Crypto : PasswordEncoder { override fun encode(rawPassword: CharSequence?): String { if (rawPassword == null) { return "" } return this.sign("SecretKey", rawPassword.toString()) } /** * Check password matching. */ override fun matches(rawPassword: CharSequence?, encodedPassword: String?): Boolean { if (rawPassword == null || encodedPassword == null) { return false } return this.sign("SecretKey", rawPassword.toString()).equals(encodedPassword) } /** * Sign with key and value. * * Not good to use password hash. * TODO: Have to use bcrypt to password. * https://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/crypto/bcrypt/BCrypt.html */ fun sign(key: String, value: String): String { val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA1") val mac = Mac.getInstance("HmacSHA1") mac.init(secretKey) val bytes = mac.doFinal(value.toByteArray()) return Base64.getEncoder().encodeToString(bytes) } }
そしてこれらを使ってログインをするための設定クラス
package net.white_azalea.todo_demo.service.secure import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter import org.springframework.security.core.userdetails.UserDetailsService @Configuration class AuthenticationConfiguration( val userDetailsService: UserDetailsService, val crypto: Crypto ) : GlobalAuthenticationConfigurerAdapter() { override fun init(auth: AuthenticationManagerBuilder?) { auth?.userDetailsService(userDetailsService) ?.passwordEncoder(crypto) } }
最後に設定これがあれば /login
に account
, password
を POST すれば認証処理が走る。
package net.white_azalea.todo_demo import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.WebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.web.authentication.AuthenticationFailureHandler import org.springframework.security.web.util.matcher.AntPathRequestMatcher @Configuration @EnableWebSecurity class SecurityConfig : WebSecurityConfigurerAdapter(false) { override fun configure(web: WebSecurity?) { web?.ignoring()?.antMatchers( "/images/**", "/css/**", "/javascript/**", "/webjars/**") } fun authFailure(): AuthenticationFailureHandler { return AuthenticationFailureHandler { request, response, exception -> response.status = 200 response.sendRedirect("/?error=Failed to access.") } } override fun configure(http: HttpSecurity?) { // 認可設定 http?.authorizeRequests() ?.antMatchers("/", "/login")?.permitAll() // index は全ユーザアクセス許可 ?.anyRequest()?.authenticated() // それ以外は認証要求 // ログイン認定 http?.formLogin() ?.loginProcessingUrl("/login") // login url ?.loginPage("/") // ログインフォームのパス ?.failureHandler(authFailure()) // ログイン失敗時のハンドラ ?.defaultSuccessUrl("/menu") // ログイン結果 ?.usernameParameter("account") // ユーザ名パラメータ ?.passwordParameter("password") // パスワードのパラメータ名 ?.and() // ログアウト設定 http?.logout() ?.logoutRequestMatcher(AntPathRequestMatcher("/logout**")) // ログアウト処理のパス ?.logoutSuccessUrl("/") // ログアウト完了時のパス } }
実際試して見たが、Cookie 嬢の JSESSION_ID なんかもきちんと更新されていたし、セッションハイジャック対策もそこそこされているのだろう。
送信フォームもこんなカンジ
<!DOCTYPE HTML> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <title>top page</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h1>Login form!</h1> <form action="/login" method="post" th:action="@{'/login'}"> <div>Account:</div> <input type="text" name="account" /> <div>Pass:</div> <input type="password" name="password" /> <button type="submit">Do login</button> </form> </body> </html>
気をつけなければならないのは @{'/login'}
の部分。
これで作成しないと、URL に CSRF ヘッダも生成されない。つまり POST が蹴られる。
やっぱ大分考慮されてるのねーってカンジである。
SPA アプリそろそろ作って見たいんじゃい(2)
次に DB 接続と Migration してみる。
SpringBoot では公式に Flyway 書いてあるので、そっちを使う。
build.gradle
にcompile("org.flywaydb:flyway-core")
追加。application.properties
に下記を記載
spring.datasource.url=jdbc:h2:./db/example;MODE=MySQL spring.jpa.hibernate.ddl-auto=validate flyway.locations=db/migration flyway.schemas=PUBLIC
src/main/resources
配下にdb/migration
ディレクトリを作って、SQL ツッコミ
注意点は、flyway はスキーマバージョン管理テーブルを作るのに、デフォルトスキーマ「PUBLIC」にアクセスするべき所を、小文字で「public」アクセスしようとして失敗する∑(゚Д゚)
-- see: http://qiita.com/niwashun/items/dc71dfba4cbb9e9eef98 CREATE SCHEMA IF NOT EXISTS "public"; CREATE TABLE `accounts` ( `id` BIGINT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` VARCHAR(255) NOT NULL, `password` VARCHAR(127) NOT NULL ); -- account and password. INSERT INTO `accounts` (`name`, `password`) VALUES ('account', '8RSglSZ8nqBtmR07hEM3E0Hdslo=');
ここまで来たら、あとは Entity とか作るだけ。
package net.white_azalea.todo_demo.repositories import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.Id @Entity(name = "accounts") data class Account( @Id @GeneratedValue val id: Long, val name: String, val password: String ) { /** * for Spring Data JPA */ constructor() : this(0, "", "") { // Nothing to do. } }
package net.white_azalea.todo_demo.repositories import org.springframework.data.jpa.repository.JpaRepository import java.util.* interface AccountRepository : JpaRepository<Account, Long> { fun findOneByName(name: String): Optional<Account> }
SPA アプリそろそろ作って見たいんじゃい(1)
話としてはこれだけ。
仕様としては、ログインと TODO だけで、特にセキュリティも考えない。
SPAの構成は、バックエンドに SpringBootWeb + Thymeleaf + SpringDataJPA + SQLite3 構成。
フロントに riot + riot-control でもしようかなと考えてる。
まずは SpringBoot を落としてくる。コレ自体は、サイト で設定してダウンロードすればよい。
選択したのは Web + JPA + Thymeleaf ここまではテンプレ作ってくれる。
で、とりあえずエンティティ作って、HelloWorld まで書いたものをぽとり。
正味 3 時間くらいかかった。
正直たかがこれだけでと思わなくない。
- Kotlin ほとんど書いてないから戸惑った
- Java と違って、
build.gradle
にspringBoot
とかいうセクションを追加しないと、ComponentScan がうまく走らないという状況に気づかなかった
いやー知らないものを詰め込みすぎた感満載。
とはいえ、Kotlin でとりあえず書けるようにはなってきたかな。
「面倒なことは Python にやらせよう」の実習課題(3章以降)
課題図書はこれ
退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング
- 作者: Al Sweigart,相川愛三
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/06/03
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る
gist ではじめてみた。
これも1日1課題以上かなー