u-ryo's blog

various information for coding...

Category: Spring

Service Worker on JHipster

| Comments

JHipsterでService WorkerでWeb Pushを、 と思っていて、Angularだから、 前やったように app.modules.tsServiceWorkerModule.register('./service-worker.js',...);してから@angular/service-workerSwPushをinjectして、 って思ってたんですが、違うんですね。 JHipsterではworkboxでService Worker使うようになってるんですね予め。 確かにindex.htmlservice-worker.jsを有効にして挙動を見てると、 cachingは綺麗に行っている様子です。 えーでも調べてみると、workboxってWeb Pushはないじゃーないですかー。

特に私の知っている範囲ではworkboxはプッシュ通知のロジックを作ってくれないので、そこは自分で書いてやる必要があります。 ServiceWorkerを簡単に書けるworkbox-swの使い方

しかも、自分のcodeとmergeする、 即ちworkboxPlugin(...)swSrc:を追加すると、 generateSWが使えず自分で書かないといけないの!? ですか? いやー、jhipsterで生成されるservice-worker.jsとか見ると、 色んなfilesにhash値?が付与されているから、 これを自分で作るというのはあり得ないでしょー。 どーしたらいーのー?!

と、途方に暮れました。

workboxを捨ててAngularのSwPushにする? いやー、でもjhipsterでのbuildではng実行されないので、 いくら.angular-cli.json"serviceWorker": trueと書いても効かないんですよー。 Angularのservice workerは生成されないわけですね。

jhipsterで生成されたbuild/www/service-worker.jsを見ていると、 上部のコメントに、

1
2
3
4
 * The rest of the code is auto-generated. Please don't update this file
 * directly; instead, make changes to your Workbox build configuration
 * and re-run your build process.
 * See https://goo.gl/2aRDsh

とあります。言われるままにそのURLを見てみると、 importScriptsというoptionがあることがわかりました。 これを指定するとどうなるのかなー、試してみます。 指定する場所は、webpackでworkboxのservice worker生成をやっているので、 webpack/webpack.prod.jsになります。 それの最下段、new WorkboxPlugin.GenerateSW({...})の中に importScripts: ['push-notifications.js']と書いてみると、 自動生成されたbuild/www/service-worker.js中に、 importScripts("push-notifications.js",...)と出ます。 なるほど。 ではこのpush-notifications.jsというのを作ってwww root folderに置き、 そこにpush notificationのlogicを書いておけばいいんじゃーん。 でも、基本的には全てのfile名はhash化?されてしまいます。 どうやって名前をhash化されないようにするの?! favicon.ico等の例から手探りで探し当てました。 webpack/webpack.common.jsnew CopyWebpackPlugin([..])に 書いておけばいーんですね。なるほど。

結局要するにworkboxのimportScriptsを使うということで、具体的には、

  1. src/main/webapp/push-notification.jsにWeb Push通知時の処理addEventListener('push', function(event) {...});と通知をclickした時の処理addEventListener('notificationclick', function(event) {...});を書いておく
  2. webpack/webpack.prod.jsnew WorkboxPlugin.GenerateSW({...})importScripts: ['push-notifications.js']を書く
  3. webpack/webpack.common.jsnew CopyWebpackPlugin([..]){ from: './src/main/webapp/push-notifications.js', to: 'push-notifications.js' },を書く

で目出度くJHipster application上でのWeb Push通知が出来ました。

尚、通知の許可を求める処理やendpoint、auth、publicKeyを求める処理では、 AngularのSwPushを使うことが出来ます。 JavaScriptでゴリゴリ書かなくてもthis.swPush.requestSubscription()だけで済むのでラクです。

Spring Auth and JWT Behind the Reverse Proxy

| Comments

JHipsterのSpring Authなapplicationを httpsのreverse proxy(nginx)の後ろに置いて、 GoogleのOAuth2でJWTな認証をしようとしました。 当然、backend serverからはGoogle APIに自分のhost名でaccessするような URLを返してしまい、Google APIから戻ってきたところでJWT認証は弾かれます。 backend serverはfrontend serverの名前を知らないんですから、 そりゃあ当然です。 こういうreverse proxyの後ろにbackend server置いてOAuth2 + JWTなんて そもそもダメなの? 何とかならないの? と調べてみると、 Spring Boot and OAuth2: redirect url over reverse proxyに、 reverse proxy側でX-Forwarded-Portとかのproxy用HTTP Response Headerを設定し、 Spring application側でserver.use-forward-headers=trueにすればいいよ、 とあったので、 じゃぁnginxではどうやるのだろうと調べると、 Nginx のリバースプロキシ設定のメモにありました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
  listen 80;
  server_name hoge.com;

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    index index.html index.htm;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://127.0.0.1:3000;
  }
}

この通りやってみると、 何故かGoogle APIにはhttp://proxy-server/...で渡っており、 じゃぁっていうんでproxy_set_header X-Forwarded-Proto https; とベタ書きしてみてもダメで、 うーんとか思っていると、 nginx でリバースプロキシするときの Tipsoffじゃなくてproxy_redirect http:// https://;という記述があったので、 試してみると、上手く行きました。 あーちなみに、proxy_set_header X-Forwarded-Proto https;も ベタ書きじゃないとダメでした。 結局うちの場合は、以下の通りになりました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        # Everything is a 404
        location / {
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
                proxy_redirect http:// https://;
                proxy_pass http://walt.mydns.bz:10022/;
        }

        # You may need this to prevent return 404 recursion.
        location = /404.html {
                internal;
        }
}

で、sudo /usr/sbin/nginx -s reloadです。

でも、これでGoogle APIから無事戻ってくるようにはなったものの、 その後「No-providerで登録」になってしまい、 まだ完成しません。 ただ、その問題は別のもののようで、一歩は進んだと思うので、記事にしました。

↑その「No-providerで登録」になってしまうのは、 backendで以下のようなerrorが出ていて。

1
2
3
4
5
6
7
8
9
10
11
javax.validation.ConstraintViolationException: Validation failed for classes [bz.mydns.walt.canmatch.domain.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
        ConstraintViolationImpl{interpolatedMessage='must match "^[_'.@A-Za-z0-9-]*$"', propertyPath=login, rootBeanClass=class bz.mydns.walt.canmatch.domain.User, messageTemplate='{javax.validation.constraints.Pattern.message}'}
]
        at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate(BeanValidationEventListener.java:140)
        at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.onPreInsert(BeanValidationEventListener.java:80)
        at org.hibernate.action.internal.EntityIdentityInsertAction.preInsert(EntityIdentityInsertAction.java:197)
        at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:75)
        at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:626)
   :
   :

何なんでしょうね。 これは、account mail addressが「w.disney@somecompany.co.jp」みたいな 「.」が入るものなんですが、それがいけないとかなのでしょうか。 というのも、フツーの「gepetto@gmail.com」みたいなmail accountなら 全く同じcodeで何の問題もなく入れるのです。 「must match」の対象が何なのか、よく分かりません。

Getting the First in Spring DATA JPA

| Comments

Spring DATA JPAで、「最新のもの一つ」を取得したかったんです。 Spring DATA JPAは、findFirstBy...とかってmethodに命名すれば 自動的にSQL作ってくれるらしいんですが(【Spring Data JPA】自動実装されるメソッドの命名ルール)、 目的のものではlogin userを自動的にparameterizeしたかったので、 それが出来ませんでした。←User objectは別途取得しておいて、 それをparameterに入れれば良かったかも、ですけど。 ともあれ、findFirstBy...で出来ないなら、 Pageableを付けるしかなさそうだ、ということで、 SQL文にはMySQLでいうところのlimit=1などはつけずに引数の最後にPageableを添え、 new PageRequest(0, 1, DESC, "to")として範囲を指定しました(org.springframework.data.domain.Sort.Direction.DESC)。

参考URLs

  1. yamkazu/springdata-jpa-example
  2. Spring Data JPA Tutorial: Pagination
  3. setMaxResults for Spring-Data-JPA annotation?

XML Marshaling in Spring Boot

| Comments

Spring Boot applicationで単一xml fileを返すREST作ってあって、 そこではxmlと同じ構造のJava bean作って返すだけで、 marshalingについてはframework側がよしなにやってくれました。 client側がHTTP Request HeaderにAccept: application/xmlとすれば。 そうでないとjsonになります。まぁそれはそれでいいんです。いいと思いました。

その後、複数xmlをまとめてZIPにして返すRESTを求められました。 そうすると、自分でXMLにmarshalingしなければなりません。 と、Gson? でもbuild.gradle見ると折角jackson読み込んでいるようなので、 jacksonでmarshalしました。new ObjectMapper()して、 mapper.write(ZipOutputStream)みたいなことすると、 一回(=one file)書いただけでstreamを勝手に? closeするようなので、 一旦Stringにしてからzos.write()しました。 また、build.gradlecompile "...jackson-dataformat-xml"も必要でした。

しかしそうすると、今度はXMLを返すRESTの方で、 返されるXMLの形が微妙に違っていました。具体的には、

  1. @XmlRootElement(name=...)で指定した名前が効かない
  2. XML Object中でList要素がnestされる (<object></object><object></object>...だったのが <object><object></object><object></object>...</object>に)

@XmlRootElementはJAXBのannotation (javax.xml.bind.annotation.XmlRootElement)で、 これが効かないというのだからJAXBが効いてないのだろうと思い、 そういえばbuild.gradlejackson-dataformat-xmlって書いたな、 というのを思い出し、 ZIP中でのXMLのmarshallingをJAXBのものでやるようにして build.gradleからjackson-dataformat-xmlを追い出したら、 元に戻りました。

JAXBでのmarshalling、ちょこっと面倒ですが、 context = JAXBContext.newInstance(Bean.class)して marshaller = context.createMarshaller()作って、 marshaller.marshal(bean, zipOutputStream)すればいいんですね。