20210504のJavaに関する記事は5件です。

Azure DevOps StarterでSpring BootのCI/CDパイプラインを作成する

Azure DevOps Starterを使ってみました。App ServiceのCI/CD環境をサクッと作れてかなり便利そうです。以下、試した手順をまとめます。 まずはDev Ops Starterを作成します。 Javaを選びます。 Springを選びます。 今回はLinux Web Appを選びました。 AuthorizeでGitHubの連携を許可します。 リポジトリ名やApp Serviceの名前を入力します。 これでGitHubにSpring BootのリポジトリとApp Serviceが作成されます。 GitHub ActionsからApp Serviceにデプロイされる形です。 App Serviceにアクセスすると以下の画面が表示されました。 デプロイ後のDevOps Starterの画面はこんな感じです。 Spring Bootのプロジェクトにhello.htmlを新しく追加してみました。HTMLは以下です。 <!Doctype html> <html> <head> <title>Hello</title> </head> <body> <h1>Hello</h1> </body> </html> DevOps Starterで作られた雛形のMyAppController.javaにhelloメソッドを加えました。 package com.myapp.root.controllers; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyAppController { @RequestMapping("/") public String index() { return "index.html"; } @RequestMapping("/hello") public String hello() { return "hello.html"; } } プッシュするとGitHub Actionsが実行されて自動的にデプロイしてくれます。便利。 ちゃんと表示されました。 私は個人開発でよくHerokuを使ってます。その理由の一つが簡単にCI/CD環境を作れることだったのですが、Azureでもこんな簡単にできると思いませんでした。今度はAzureを使ってみようかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring BootでSpring MVC関連のログを制す

Spring BootでWebアプリケーションを作ろうとすると、「組み込みTomcat(サーブレットエンジン)」×「Spring MVC」の組み合わせでアプリケーションを作る事も多いと思います。その際に・・・自分が思っている通りに動かない時皆さんはどうしているでしょうか? Spring BootやSpring MVCに詳しい方はフレームワークのクラスに適当にブレークポイントを指定して、どこまで処理が想定通りに動いているか確認する!!という強者も中にはいるかも!?しれませんが・・・Spring Boot(Spring MVC)初心者の方は、何が起きているのかわからず・・・エラー解析に多くの(無駄な)時間をかけてしまうことがあるのではないかと思います。 どうすればよい? エラーの内容によって対処は変わりますが、ここでは・・・アプリケーション自体は起動したけど、自前で用意したControllerの呼び出し結果が想定と異なる場合に、まずはどうするのがよいのか?というところを紹介したいと思います。本エントリーでは「404 NOT FOUND」が発生した場合のエラー解析を例に話を進めさせてもらいます。 結論→詳細なログを出すべし!! Spring Boot(Spring MVC)初心者の方は、思い通り動くアプリケーションが作れるようになるまでは・・・とりあえず詳細なログ(DEBUG/TRACEレベルのログ)を出しておくのがよいのかな〜と思います。 WARNING: 開発環境以外では、基本的にはINFO以上にしてください。 src/main/resources/application.properties # Web関連のロガーのログレベルをDEBUG or TRACEへ logging.level.web=trace # Spring MVCへのリクエスト/レスポンスログの詳細情報出力を有効化 spring.mvc.log-request-details=true TIPS: 「web」というロガー名は何者? Spring Bootでは「Logger Groups」という仕組みがサポートされており、「web」は組み込みのLogger Groupで、Web関連のロガーのログレベルをまとめて指定することができるようになっています。 デフォルトの動作を知る Spring Bootアプリケーションを Spring Initializrから作成した場合、ログの出力レベルは「INFO」になっています。この状態でアプリケーションを起動し、存在しないパスへリクエストを送った時に出力されるログを見てみましょう。 まずは、アプリケーション起動時には以下のようなログ(Tomcatが起動したことがわかるログ)が出力されます。 コンソール(起動時) 2021-05-04 14:17:03.094 INFO 76330 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2021-05-04 14:17:03.101 INFO 76330 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2021-05-04 14:17:03.102 INFO 76330 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.45] 2021-05-04 14:17:03.144 INFO 76330 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2021-05-04 14:17:03.144 INFO 76330 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 668 ms 2021-05-04 14:17:03.262 INFO 76330 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2021-05-04 14:17:03.394 INFO 76330 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 起動後にクライアントから存在しないパスへアクセスしてみます。 コンソール(クライアント) % curl -D - "http://localhost:8080/home?name=Kazuki" -H "X-Tracking-Id: $(uuidgen)" HTTP/1.1 404 Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked Date: Tue, 04 May 2021 05:17:19 GMT {"timestamp":"2021-05-04T05:17:19.058+00:00","status":404,"error":"Not Found","message":"","path":"/home"} curlコマンドの結果よりクライアントには「404: NOT FOUND」が返却されたことがわかりますが、サーバのログをみてみると・・・ DispatcherServlet が初期化されたことがわかるログは出ていますが、それ以外の情報はログに出ていません。 コンソール(初回アクセス時) ...(省略)... 2021-05-04 14:17:19.011 INFO 76330 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2021-05-04 14:17:19.012 INFO 76330 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2021-05-04 14:17:19.013 INFO 76330 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms これは・・・エラー原因がクライアント側に存在するため(=存在しないパスへアクセスしてきているため)、サーバ側の問題ではないのでログは出力していないという感じだと思います。 テストによりアプリケーションの品質が担保されているプロダクション環境などにおいては、クライアント側の問題で発生する事象をログを出力しないのは適切だと言えますが、絶賛開発中(しかもフレームワーク利用の初心者が開発中)であればログを出した方がよいことがあるはずです。 出力レベルをDEBUGにしてみる まずは、Logger Groupである「web」の出力レベルをDEBUGにするとどのような情報が出力されるか確認してみましょう。 src/main/resources/application.properties # Web関連のロガーのログレベルをDEBUGへ logging.level.web=debug # Spring MVCへのリクエスト/レスポンスログの詳細情報出力はいったん無効化 spring.mvc.log-request-details=false Tomcat起動時に初期化した「サーブレットフィルタ」「サーブレット」「リクエストハンドラ」などの情報が出力されるようになります。 コンソール(起動時) ...(省略)... 2021-05-04 14:18:09.048 INFO 76343 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 642 ms +2021-05-04 14:18:09.061 DEBUG 76343 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105, customFilterWithDI urls=[/*] order=2147483647 +2021-05-04 14:18:09.061 DEBUG 76343 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Mapping servlets: dispatcherServlet urls=[/] 2021-05-04 14:18:09.154 INFO 76343 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' +2021-05-04 14:18:09.159 DEBUG 76343 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice +2021-05-04 14:18:09.190 DEBUG 76343 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : 3 mappings in 'requestMappingHandlerMapping' +2021-05-04 14:18:09.204 DEBUG 76343 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Patterns [/webjars/**, /**] in 'resourceHandlerMapping' +2021-05-04 14:18:09.209 DEBUG 76343 --- [ main] .m.m.a.ExceptionHandlerExceptionResolver : ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice 2021-05-04 14:18:09.267 INFO 76343 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 初回アクセス時にDispatcherServletが初期化され、初期化時に検出したSpring MVCのコンポーネントの情報などが出力されるようになります。 コンソール(初回アクセス時=サーブレット初期化時) ...(省略)... 2021-05-04 14:22:46.885 INFO 76343 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2021-05-04 14:22:46.886 INFO 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' +2021-05-04 14:22:46.886 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected StandardServletMultipartResolver +2021-05-04 14:22:46.886 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected AcceptHeaderLocaleResolver +2021-05-04 14:22:46.886 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected FixedThemeResolver +2021-05-04 14:22:46.887 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@56db55b7 +2021-05-04 14:22:46.887 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.support.SessionFlashMapManager@1336fef8 +2021-05-04 14:22:46.887 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data 2021-05-04 14:22:46.888 INFO 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms リクエスト時には、Spring(Spring Boot)がどのように処理して最終的に「404: NOT FOUND」にしたのかがわかるような情報が出力されます。 コンソール(リクエスト時) +2021-05-04 14:22:46.895 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/home?name=Kazuki", parameters={masked} +2021-05-04 14:22:46.906 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [Classpath [META-INF/resources/], Classpath [resources/], Classpath [static/], Classpath [public/], ServletContext [/]] +2021-05-04 14:22:46.908 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.r.ResourceHttpRequestHandler : Resource not found +2021-05-04 14:22:46.908 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND +2021-05-04 14:22:46.912 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error?name=Kazuki", parameters={masked} +2021-05-04 14:22:46.913 DEBUG 76343 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest) +2021-05-04 14:22:46.936 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json] +2021-05-04 14:22:46.936 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Tue May 04 14:22:46 JST 2021, status=404, error=Not Found, message=, path=/home}] +2021-05-04 14:22:46.960 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404 ざっくり説明すると・・・・ 2021-05-04 14:22:46.895 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/home?name=Kazuki", parameters={masked} では、GET /home というパスにリクエストがあったことが確認できます。 2021-05-04 14:22:46.906 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [Classpath [META-INF/resources/], Classpath [resources/], Classpath [static/], Classpath [public/], ServletContext [/]] 2021-05-04 14:22:46.908 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.r.ResourceHttpRequestHandler : Resource not found 2021-05-04 14:22:46.908 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND では、GET /home に対応する「リクエストハンドラ(Controllerなど)」が見つからなかったことが確認できます。 2021-05-04 14:22:46.912 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error?name=Kazuki", parameters={masked} 2021-05-04 14:22:46.913 DEBUG 76343 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest) 2021-05-04 14:22:46.936 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json] 2021-05-04 14:22:46.936 DEBUG 76343 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Tue May 04 14:22:46 JST 2021, status=404, error=Not Found, message=, path=/home}] 2021-05-04 14:22:46.960 DEBUG 76343 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404 では、エラー応答を行う「リクエストハンドラ(Spring Bootデフォルトのハンドラ)」へリクエストを転送し、そのリクエストハンドラによってエラー応答(JSON)が行われていることが確認できます。が・・・しかし、ログレベルをDEBUGにすることで「404: NOT FOUND」が応答された直接的な原因や処理の流れは(何となく)わかりますが、このログだけでは本質的な原因を特定することはできていません。 出力レベルをTRACEにしてみる つぎに、Logger Groupである「web」の出力レベルをTRACEにするとどのような情報が出力されるか確認してみましょう。 src/main/resources/application.properties # Web関連のロガーのログレベルをTRACEへ logging.level.web=trace # Spring MVCへのリクエスト/レスポンスログの詳細情報出力はいったん無効化 spring.mvc.log-request-details=false Tomcat起動時に初期化した「サーブレットフィルタ」「サーブレット」「リクエストハンドラ」などの詳細な情報が出力されるようになります。 コンソール(起動時) ...(省略)... 2021-05-04 14:48:57.491 INFO 76466 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 703 ms +2021-05-04 14:48:57.493 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Added existing Servlet initializer bean 'dispatcherServletRegistration'; order=2147483647, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration.class] +2021-05-04 14:48:57.498 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'characterEncodingFilter'; order=-2147483648, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class] +2021-05-04 14:48:57.498 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'formContentFilter'; order=-9900, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.class] +2021-05-04 14:48:57.498 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'requestContextFilter'; order=-105, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class] 2021-05-04 14:48:57.501 DEBUG 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105 2021-05-04 14:48:57.501 DEBUG 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Mapping servlets: dispatcherServlet urls=[/] 2021-05-04 14:48:57.594 INFO 76466 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2021-05-04 14:48:57.600 DEBUG 76466 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice +2021-05-04 14:48:57.630 TRACE 76466 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : + c.e.c.c.DemoController: + {GET [/hello]}: hello(String,String) +2021-05-04 14:48:57.633 TRACE 76466 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : + o.s.b.a.w.s.e.BasicErrorController: + { [/error]}: error(HttpServletRequest) + { [/error], produces [text/html]}: errorHtml(HttpServletRequest,HttpServletResponse) 2021-05-04 14:48:57.635 DEBUG 76466 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : 3 mappings in 'requestMappingHandlerMapping' 2021-05-04 14:48:57.643 DEBUG 76466 --- [ main] o.s.w.s.h.BeanNameUrlHandlerMapping : Detected 0 mappings in 'beanNameHandlerMapping' +2021-05-04 14:48:57.645 TRACE 76466 --- [ main] o.s.w.s.f.support.RouterFunctionMapping : 0 RouterFunction(s) in 'routerFunctionMapping' +2021-05-04 14:48:57.652 TRACE 76466 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped [/webjars/**] onto ResourceHttpRequestHandler [Classpath [META-INF/resources/webjars/]] +2021-05-04 14:48:57.653 TRACE 76466 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped [/**] onto ResourceHttpRequestHandler [Classpath [META-INF/resources/], Classpath [resources/], Classpath [static/], Classpath [public/], ServletContext [/]] 2021-05-04 14:48:57.653 DEBUG 76466 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Patterns [/webjars/**, /**] in 'resourceHandlerMapping' ...(省略)... どのような情報が詳細かされたかというと・・・ 2021-05-04 14:48:57.493 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Added existing Servlet initializer bean 'dispatcherServletRegistration'; order=2147483647, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration.class] 2021-05-04 14:48:57.498 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'characterEncodingFilter'; order=-2147483648, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class] 2021-05-04 14:48:57.498 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'formContentFilter'; order=-9900, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.class] 2021-05-04 14:48:57.498 TRACE 76466 --- [ main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'requestContextFilter'; order=-105, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class] では、アプリケーションに適用されたサーブレットフィルタに関する詳細な情報を確認することができます。 2021-05-04 14:48:57.630 TRACE 76466 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : c.e.c.c.DemoController: {GET [/hello]}: hello(String,String) 2021-05-04 14:48:57.633 TRACE 76466 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : o.s.b.a.w.s.e.BasicErrorController: { [/error]}: error(HttpServletRequest) { [/error], produces [text/html]}: errorHtml(HttpServletRequest,HttpServletResponse) では、アプリケーションに適用されたController(アノテーション駆動のハンドラ)に関する詳細な情報を確認することができます。今回のケースで言えば、GET /home に対応するControllerが適用されていないことがわかります。もし GET /home に対応するControllerを作っているのに「404: NOT FOUND」が出るのであれば、おそらくControllerの作り方に誤り(例えば・・・@Controllerアノテーションをつけ忘れた、GET /home ではなく GET /homa になってしまっていた 等)がある可能性を疑うことができるはずです。 NOTE: 本エントリーしてでは解説は扱いしますが、↓のログでは、Controller以外のハンドラに関する詳細な情報を確認することができます。 2021-05-04 14:48:57.645 TRACE 76466 --- [ main] o.s.w.s.f.support.RouterFunctionMapping : 0 RouterFunction(s) in 'routerFunctionMapping' 2021-05-04 14:48:57.652 TRACE 76466 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped [/webjars/**] onto ResourceHttpRequestHandler [Classpath [META-INF/resources/webjars/]] 2021-05-04 14:48:57.653 TRACE 76466 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped [/**] onto ResourceHttpRequestHandler [Classpath [META-INF/resources/], Classpath [resources/], Classpath [static/], Classpath [public/], ServletContext [/]] 初回アクセス時にDispatcherServletが初期化され、初期化時に検出したSpring MVCのコンポーネントの情報などが出力されるようになります(ここはDEBUG時と出力形式が異なりますが情報量自体に大きな違いはなさそうです)。 コンソール(初回アクセス時=サブレット初期化時) ...(省略)... 2021-05-04 15:08:46.276 INFO 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' +2021-05-04 15:08:46.276 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.multipart.support.StandardServletMultipartResolver@7b11d29a +2021-05-04 15:08:46.276 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver@40c65638 +2021-05-04 15:08:46.276 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.theme.FixedThemeResolver@63250c7d +2021-05-04 15:08:46.277 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected DefaultRequestToViewNameTranslator +2021-05-04 15:08:46.277 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected SessionFlashMapManager 2021-05-04 15:08:46.277 DEBUG 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data ...(省略)... リクエスト時には、Spring(Spring Boot)がどのように処理して最終的に「404: NOT FOUND」にしたのかがわかるような情報がより詳細に出力されます。 コンソール(リクエスト時) +2021-05-04 15:08:46.284 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/home?name=Kazuki", parameters={masked}, headers={masked} in DispatcherServlet 'dispatcherServlet' +2021-05-04 15:08:46.289 TRACE 76513 --- [nio-8080-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to HandlerExecutionChain with [ResourceHttpRequestHandler [Classpath [META-INF/resources/], Classpath [resources/], Classpath [static/], Classpath [public/], ServletContext [/]]] and 3 interceptors 2021-05-04 15:08:46.290 DEBUG 76513 --- [nio-8080-exec-1] o.s.w.s.r.ResourceHttpRequestHandler : Resource not found +2021-05-04 15:08:46.291 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : No view rendering, null ModelAndView returned. 2021-05-04 15:08:46.291 DEBUG 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND, headers={masked} +2021-05-04 15:08:46.303 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error?name=Kazuki", parameters={masked}, headers={masked} in DispatcherServlet 'dispatcherServlet' +2021-05-04 15:08:46.304 TRACE 76513 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : 2 matching mappings: [{ [/error]}, { [/error], produces [text/html]}] +2021-05-04 15:08:46.305 TRACE 76513 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest) +2021-05-04 15:08:46.315 TRACE 76513 --- [nio-8080-exec-1] o.s.web.method.HandlerMethod : Arguments: [org.apache.catalina.core.ApplicationHttpRequest@c1b8e4a] 2021-05-04 15:08:46.330 DEBUG 76513 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json] +2021-05-04 15:08:46.331 TRACE 76513 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Tue May 04 15:08:46 JST 2021, status=404, error=Not Found, message=, path=/home}] +2021-05-04 15:08:46.359 TRACE 76513 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerAdapter : Applying default cacheSeconds=-1 +2021-05-04 15:08:46.360 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : No view rendering, null ModelAndView returned. 2021-05-04 15:08:46.360 DEBUG 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404, headers={masked} 更に詳細な情報を出力してみる リクエストに対応するControllerを指定する方法は、代表的な方法は「リクエストメソッド(GET, POSTなど)」と「リクエストパス(/home など)」の組み合わせで一致させることですが、更に「リクエストパラメータ(name=value)」や「リクエストヘッダ(name: value)」などを条件に指定することもあります(具体的には・・・画面を扱うアプリケーションにて、一つの入力フォーム内に複数のサブミットボタンが配置されているような場合は、押されたボタンの名前で呼び出すハンドラを切り替えるような設計にすることがあるため)。そのため、Logger Groupである「web」の出力レベルをTRACEにするだけではエラー解析に必要な情報が不足することがあります。そのような場合は・・・・「Spring MVCへのリクエスト/レスポンスログの詳細情報出力」を有効化してみてください。 src/main/resources/application.properties # Web関連のロガーのログレベルをTRACEへ logging.level.web=trace # Spring MVCへのリクエスト/レスポンスログの詳細情報出力を有効化 spring.mvc.log-request-details=true 「詳細情報出力を有効化」すると、{masked}となっていた部分に実際の値(リクエストパラメータ値、リクエストヘッダ値、レスポンスヘッダ値)が出力されます。 -2021-05-04 15:08:46.284 TRACE 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/home?name=Kazuki", parameters={masked}, headers={masked} in DispatcherServlet 'dispatcherServlet' +2021-05-04 15:21:20.327 TRACE 76565 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/home?name=Kazuki", parameters={name:[Kazuki]}, headers={host:[localhost:8080], user-agent:[curl/7.64.1], accept:[*/*], x-tracking-id:[EA4CE16E-5D5E-4959-BA77-228F08F61B0D]} in DispatcherServlet 'dispatcherServlet' ...(省略)... -2021-05-04 15:08:46.291 DEBUG 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND, headers={masked} +2021-05-04 15:21:20.349 DEBUG 76565 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND, headers={Vary:[Origin, Access-Control-Request-Method, Access-Control-Request-Headers], Vary:[Origin, Access-Control-Request-Method, Access-Control-Request-Headers], Vary:[Origin, Access-Control-Request-Method, Access-Control-Request-Headers]} ...(省略)... -2021-05-04 15:08:46.360 DEBUG 76513 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404, headers={masked} +2021-05-04 15:21:20.432 DEBUG 76565 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404, headers={Vary:[Origin, Access-Control-Request-Method, Access-Control-Request-Headers], Vary:[Origin, Access-Control-Request-Method, Access-Control-Request-Headers], Vary:[Origin, Access-Control-Request-Method, Access-Control-Request-Headers], Content-Type:[application/json], Transfer-Encoding:[chunked], Date:[Tue, 04 May 2021 06:21:20 GMT]} おまけ:Spring Boot Actuatorで出力レベルを変更する プロダクション環境でログレベルを変えることは基本的にはないと思いますが、ステージング環境や各種テスティング環境にて、一時的に出力レベルを変更してアプリケーション内部の動作を確認したい場合は、Spring Boot ActuatorのLoggersエンドポイントを利用してログの出力レベルを変更することもできます。 依存アーティファクトとして、spring-boot-starter-actuatorを追加します。 pom.xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> loggersのWebエンドポイントを有効化します。 src/main/resources/application.properties # loggersのwebエンドポイントを有効化 management.endpoints.web.exposure.include=info,loggers 起動後に現在の出力レベルを確認しましょう。 % curl -D - -X GET http://localhost:8080/actuator/loggers/web HTTP/1.1 200 Content-Type: application/vnd.spring-boot.actuator.v3+json Transfer-Encoding: chunked Date: Tue, 04 May 2021 07:04:57 GMT {"configuredLevel":null,"members":["org.springframework.core.codec","org.springframework.http","org.springframework.web","org.springframework.boot.actuate.endpoint.web","org.springframework.boot.web.servlet.ServletContextInitializerBeans"]} 出力レベルをTRACEにしてみます。 % curl -D - -X POST http://localhost:8080/actuator/loggers/web -d '{"configuredLevel": "TRACE"}' -H "Content-Type: application/json" HTTP/1.1 204 Date: Tue, 04 May 2021 07:05:37 GMT 「TRACE」に変更されたことを確認しましょう。 % curl -D - -X GET http://localhost:8080/actuator/loggers/web HTTP/1.1 200 Content-Type: application/vnd.spring-boot.actuator.v3+json Transfer-Encoding: chunked Date: Tue, 04 May 2021 07:05:39 GMT {"configuredLevel":"TRACE","members":["org.springframework.core.codec","org.springframework.http","org.springframework.web","org.springframework.boot.actuate.endpoint.web","org.springframework.boot.web.servlet.ServletContextInitializerBeans"]} おまけ:Developer Toolsでローカルの出力レベルを変更する アプリケーション開発をチームで行う場合、ソースコードはGitなどのバージョン管理システムを使うことになると思いますが、ローカルで試しにログレベルを変えて(例えばTRACEレベルに変更して)、そのまままリモートへpushしてしまった!!という経験(事故!?)はないですか? ログは開発者の知識レベルによって出したい箇所や出したいレベルが変わってくるので、そのような場合は、Spring Bootが提供しているDeveloper Toolsを利用して、ローカル環境でのみ読み込まれる設定ファイルに個人が望むログ定義を指定するという方法があります。 依存アーティファクトとして、spring-boot-devtoolsを追加します。 pom.xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> 個人専用のプロパティファイルを作成し、そのファイルに出力レベル(ここではTRACE)を指定します。 % mkdir -p $HOME/.config/spring-boot && echo "logging.level.web=trace" > $HOME/.config/spring-boot/spring-boot-devtools.properties % view $HOME/.config/spring-boot/spring-boot-devtools.properties logging.level.web=trace この状態でIDE上でアプリケーションを起動すると、「Logger Group」のwebの出力レベルがTRACEになります。 動作検証バージョン Spring Boot 2.4.5 まとめ 今回はSpring MVCに関わるログ出力に関して記載しましたが、ログ(特にエラーログ)はアプリケーション開発および保守していく上で非常に大事な要素になります。Spring Framework(Spring Frameworkベースの各種ライブラリ)も開発を手助けする各種ログを出力してくれているので、エラー解析やフレームワーク内部の動作検証をする際には、出力レベルを下げて実行してみると、これまで見えていなかったことが見えてくると思います。また「Logger Group」という仕組みでロガーをグループ化することで、関連するロガーを集約して管理できるのも地味にありがたいかも〜と思いました。 参考サイト https://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java: nullにまつわる興味深い事実

null安全でない言語は、もはやレガシー言語だ という少し前の記事を読んだのですが、依然そんなレガシー言語と日々対峙していて改めてnullにまつわる話を見つけたので改めて向き合ってみた話。 Javaにおける null にまつわる興味深い事実 - Interesting facts about null in Java ほとんどのプログラミング言語はnullとの戦いである。nullに悩まされないプログラマーはいない。 Javaでは、nullはjava.lang.NullPointerExceptionと関わる。それはjava.langパッケージ内のクラス。nullを使用したり使用しなかったりして何らかの操作を行おうとすると呼び出され、時にはどこで起こったのかさえわからないこともある。 で、Javaプログラマーが知っておくべきJavaのnullに関する重要なポイントが以下だとのこと。ざっくり手を動かしてみた。 1. "nullは大文字小文字を区別する" Javaではnullは リテラル であり、大文字小文字を区別する。小文字のみ。C言語のようにNULLや0とは書けない。 // 以下はコンパイルエラー : can't find symbol 'NULL' Object obj = NULL; // こちらは動く。 Object obj1 = null; 2. "参照変数" Javaでは、どの参照変数もデフォルト値はnullである。 private static Object obj; public static void main(String args[]){ // null がプリントされる System.out.println("Value of object obj is : " + obj); } 3. "nullの型" nullはオブジェクトでもなければ型でもない。任意の参照型に割り当てることができる特別な値。 任意の型にキャストすることができる。 // null は String に代入できる。 String str = null; // Integer にも代入できる。 Integer itr = null; // Double にも代入できる。 Double dbl = null; // String にキャストできる。 String myStr = (String) null; // Integer にキャストできる。 Integer myItr = (Integer) null; // はい。これもキャストできる。No error Double myDbl = (Double) null; 4. "オートボクシングとアンボクシング" オートボクシングとアンボクシング の操作中、プリミティブなボクシングされたデータタイプにnull値が割り当てられた場合、NullpointerExceptionが投げられる。 // Integer は null になりえる。 Integer i = null; // こちらはヌルポ int a = i; 5. "instanceof 演算子" javaのinstanceof演算子は、オブジェクトが指定された型(クラスまたはサブクラスまたはインターフェース)のインスタンスであるかどうかをテストするために使用される。実行時、式の値がnullでなければ、instanceof演算子の結果はtrueになる。 Integer i = null; Integer j = 10; String s = "10"; // false System.out.println(i instanceof Integer); // true System.out.println(j instanceof Integer); // これはコンパイルエラー //System.out.println(s instanceof Integer); 6. "staticメソッドとnon-staticメソッド" null値を持つ参照変数に対してnon-staticメソッドを呼び出すことはできない。NullPointerExceptionが投げられる。staticメソッドは、staticバインディングを使用して結合されているので、NullPointerExceptionを投げることはない。 App obj= null; obj.staticMethod(); obj.nonStaticMethod(); private static void staticMethod(){ System.out.println("static method. "); } private void nonStaticMethod() { System.out.println(" Non-static method. "); } 7. "== と != " Javaでは、比較演算子はnullと一緒に使うことができる。これによりnullをチェックする。 // true System.out.println(null==null); // false System.out.println(null!=null); if(s==null || s.equals("")){ 上記のコードのnullまたは空文字であるかの判定条件の順序を逆にすると、値がnullの場合に実行時エラーが発生する。 全体 App.java public class App { static Object _obj; public static void main(String[] args) throws Exception { System.out.println("Hello, World!"); func1(); func2(); func3(); func4(); func5(); func6(); func7(); } private static void func1() { // 以下はコンパイルエラー : can't find symbol 'NULL' // Object obj = NULL; // こちらは動く。 Object obj1 = null; System.out.println("事実1: null は小文字。Case sensitive."); } private static void func2() { // "null" がプリントされる。 System.out.println("事実2: どの参照変数もデフォルト値は null。Object _obj の値は : " + _obj); } private static void func3() { // null は String に代入できる。 String str = null; System.out.println("事実3: "+str+"は任意の型にキャストすることができる。"); // Integer にも代入できる。 Integer itr = null; System.out.println("なので Integer に代入しても" + itr); // Double にも代入できる。 Double dbl = null; System.out.println("Double に代入しても" + dbl); // String にキャストできる。 String myStr = (String) null; System.out.println("String にキャストしても" + myStr); // Integer にキャストできる。 Integer myItr = (Integer) null; System.out.println("Integer にキャストしても" + myItr); // うん。これもキャストできる。No error Double myDbl = (Double) null; System.out.println("Double にキャストしても" + myDbl); } private static void func4() { // Integer は null になりえる。 Integer i = null; // オブジェクトを値型に戻す。Unboxing null to integer throws NullpointerException // Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "i" is null // at App.func4(App.java:48) // at App.main(App.java:7) // https://ja.wikipedia.org/wiki/ボックス化 // https://qiita.com/chihiro/items/870eca6e911fa5cd8e58 try { int a = i; } catch (NullPointerException e) { System.out.println("事実4: アンボクシング時にNullPointerException。"); System.out.println("--------------------------------------------------------------------------------"); e.printStackTrace(); System.out.println("--------------------------------------------------------------------------------"); } } private static void func5() { Integer i = null; Integer j = 10; // false がプリントされる System.out.println("事実5: 値がnullでなければ、instanceof演算子の結果はtrue。nullでは"); System.out.println("--------------------------------------------------------------------------------"); System.out.println(i instanceof Integer); System.out.println("--------------------------------------------------------------------------------"); // もちろん true System.out.println("こちらは"); System.out.println("--------------------------------------------------------------------------------"); System.out.println(j instanceof Integer); System.out.println("--------------------------------------------------------------------------------"); // これはコンパイルエラー //System.out.println(s instanceof Integer); } private static void func6(){ App obj= null; obj.staticMethod(); try { obj.nonStaticMethod(); } catch (NullPointerException e) { System.out.println("こちらは例外を投げる。"); System.out.println("--------------------------------------------------------------------------------"); e.printStackTrace(); System.out.println("--------------------------------------------------------------------------------"); } } private static void staticMethod(){ System.out.println("事実6: static method。null 参照静的メソッドは、静的バインディングを使用して結合されているので、NullPointerExceptionを投げない。"); } private void nonStaticMethod() { System.out.println(" Non-static method- "); } private static void func7(){ // trueを返す。 System.out.println("事実7: 比較演算子はnullと一緒に使うことができる。"); System.out.println("だからこれは "); System.out.println(null==null); // falseを返す。 System.out.println("これは "); System.out.println(null!=null); } } その出力 PS C:\Users\s5551> & 'c:\Users\s5551\.vscode\extensions\vscjava.vscode-java-debug-0.33.1\scripts\launcher.bat' 'C:\Program Files\Java\jdk-16.0.1\bin\java.exe' '--enable-preview' '-XX:+ShowCodeDetailsInExceptionMessages' '-Dfile.encoding=UTF-8' '-cp' 'C:\Users\s5551\AppData\Local\Temp\vscodesws_de01f\jdt_ws\jdt.ls-java-project\bin' 'App' Hello, World! 事実1: null は小文字。Case sensitive. 事実2: どの参照変数もデフォルト値は null。Object _obj の値は : null 事実3: nullは任意の型にキャストすることができる。 なので Integer に代入してもnull Double に代入してもnull String にキャストしてもnull Integer にキャストしてもnull Double にキャストしてもnull 事実4: アンボクシング時にNullPointerException。 -------------------------------------------------------------------------------- java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "i" is null at App.func4(App.java:63) at App.main(App.java:9) -------------------------------------------------------------------------------- 事実5: 値がnullでなければ、instanceof演算子の結果はtrue。nullでは -------------------------------------------------------------------------------- false -------------------------------------------------------------------------------- こちらは -------------------------------------------------------------------------------- true -------------------------------------------------------------------------------- 事実6: static method。null 参照静的メソッドは、静的バインディングを使用して結合されているので、NullPointerExceptionを投げない。 こちらは例外を投げる。 -------------------------------------------------------------------------------- java.lang.NullPointerException: Cannot invoke "App.nonStaticMethod()" because "obj" is null at App.func6(App.java:92) at App.main(App.java:11) -------------------------------------------------------------------------------- 事実7: 比較演算子はnullと一緒に使うことができる。 だからこれは true これは false まとめ Javaに対する私の最近の感想は以下。 冒頭の「レガシー言語だ」記事、コメントの議論も色々面白かった。同じ意味で「はてぶ」のコメントも興味深いのでメモとして貼っておく。 まあそんな変な話題から入ってしまったが、とはいえJavaも日々進化は続いていて、その挾間でサラリーマンJava開発者(私)は色々複雑な気持ちになる。闇と光。 Javaどうなってゆくのでしょうねえと既に連休明けを思い浮かべて悶々としつつ...。以上なにがしかお楽しみいただけたらさいわいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Aerospikeことはじめ ~JavaからAerospikeに接続・CRUD操作~

はじめに 前回はローカルマシンにAerospikeを(Dockerイメージで)入れたので 今回はJavaからAerospikeにアクセスしてみる。 環境 ソフトウェア バージョン OS Windows10 Docker Engine 20.10.5 Java 11 AerospikeにJavaでアクセスするには Javaのバージョンが8以上である必要がある。 準備 1. Javaクライアントライブラリの取得 まず初めにAerospikeのJavaクライアントライブラリを入手する。 MavenやGradle、Ivyで入手可能。 今回はGradleを使用。 build.gradleに以下の依存ライブラリ定義を追加。 build.gradle repositories { mavenCentral() } dependencies { compile "com.aerospike:aerospike-client:5.0.0" } この定義をいれてGradleのbuildを実行すれば ライブラリがDLされる。 2. データの準備 AerospikeのDockerコンテナに入り、 AQL(Aerospike Query Language) でデータを入れる。 (AerospikeのDockerコンテナ、AQLについては、前回の記事参照) データ挿入 aql> INSERT INTO test.NintenGames (PK, Name, Release) VALUES ('SMB', 'SuperMarioBros', 1983) OK, 1 record affected. aql> INSERT INTO test.NintenGames (PK, Name, Release) VALUES ('LOZ', 'LegendOfZelda', 1986) OK, 1 record affected. aql> INSERT INTO test.NintenGames (PK, Name, Release) VALUES ('PKM', 'Pockemon', 1998) OK, 1 record affected. aql> SELECT Name, Release FROM test.NintenGames +------------------+----------+ | Name | Release | +------------------+----------+ | "LegendOfZelda" | 1986 | | "SuperMarioBros" | 1983 | | "Pockemon" | 1998 | +------------------+----------+ 3 rows in set (0.101 secs) OK こんな感じのデータができた。 実装 1. 接続 接続方法は下記のとおり。 単一のノードを指定する方法と 複数のノードを指定する方法がある。 複数ノード指定した場合は、指定されたホストのどれかに接続する。 一つも接続できなかった場合はAerospikeExceptionがスローされる。 // 単一ノード指定 AerospikeClient client = new AerospikeClient("127.0.0.1", 3000); // 複数ノード指定 Host[] hosts = new Host[] { new Host("127.0.0.1", 3000), new Host("another.host", 3000), new Host("and.another.host", 3000) }; ClientPolicy clPolicy = new ClientPolicy(); AerospikeClient clientMultiTarget = new AerospikeClient(clPolicy, hosts); 接続ポートはデフォルトでは3000番 Aerospikeサーバ側の以下の設定ファイルで確認したり設定変更が可能。 /etc/aerospike/aerospike.conf また、 TLS接続や接続時のユーザ認証などもできるが今回は省略。 (ユーザ認証はEnterpriseEditionのみ) 2. トランザクション 2-1. 挿入 Recordの挿入は以下のような流れ。 書き込みポリシー生成 キー生成 Binオブジェクト生成 挿入(client.put呼び出し) 挿入コード // ①書き込みポリシー生成 WritePolicy wPolicy = new WritePolicy(); wPolicy.timeoutDelay = 50; // 50 millisecond timeout. wPolicy.expiration = 100; // 100 seconds TTL. // ②キーを生成 // 引数をNamespace(DBに相当)、Set(Tableに相当)、PKの順に指定する Key key = new Key("test", "NintenGames", "KBY"); // ③書き込むBin(Columnに相当)を生成 Bin binName = new Bin("Name", "Kirby"); Bin binRelease = new Bin("Release", 1992); // ④挿入 client.put(wPolicy, key, binName, binRelease); ①では、書き込みポリシー設定として、接続タイムアウトやRecordのTTL(expiration)などを設定できる。 TTLは秒で指定。0を指定した場合、Namespaceの"default-ttl"の値が設定される。 期限無しにしたい場合は、-1を指定する。 他にも色々設定できる。APIリファレンス参照。 ②では、Namespace(DBに相当)、Set(Tableに相当)、PKを持つKeyオブジェクトを生成。 このKeyオブジェクトでどこに挿入するかを指定するわけだ。 ③では、レコードの値となるBin(Columnに相当)を生成。 ④で挿入される。第3引数以降はBin型の可変長引数になっているのでいくつでも指定可能。 このコード実行後のDB内容は以下の通り。 DB内容 aql> SELECT Name, Release FROM test.NintenGames +------------------+----------+ | Name | Release | +------------------+----------+ | "LegendOfZelda" | 1986 | | "SuperMarioBros" | 1983 | | "Kirby" | 1992 | | "Pockemon" | 1998 | +------------------+----------+ 4 rows in set (0.103 secs) Kirbyが追加された!⭐ 2-2. 参照 Recordの参照は以下のような流れ。 読み込みポリシー生成 キー生成 Recordを取得 RecordからBinの値を取得 参照コード // ①読み込みポリシー指定 ScanPolicy sPolicy = new ScanPolicy(); sPolicy.timeoutDelay = 50; // 50 millisecond timeout. // ②キーを生成 // 引数をnamespace(DBに相当)、set(Tableに相当)、PKの順に指定する Key key2 = new Key("test", "NintenGames", "LOZ"); // ③Recordを取得 Record record = client.get(sPolicy, key2); // ④RecordからBinの値を取得 System.out.printf("Name=[%s] Release=[%d]", record.getString("Name"), record.getInt("Release")); ①では、読み込みポリシー設定として、接続タイムアウトや1秒あたりの読み込みレコード制限数などを指定できる。 ②は書き込み時と同様、参照したいRecordのキーを生成。 ③でRecordを取得。 ④でRecordオブジェクトのget〇〇メソッドでBinの値を取得。 get〇〇の引数にはBin名を指定。 〇〇の部分はINSERT時に入れたデータの型と一致したメソッドを呼ばないと例外が発生する。 このコード実行結果は以下の通り。 実行結果 Name=[LegendOfZelda] Release=[1986] LegendOfZeldaの情報が取れた!?️ 2-3. 削除 削除は、削除したいキー情報を指定するだけ。 削除コード // ①削除対象キー生成 Key key3 = new Key("test", "NintenGames", "PKM"); // ②削除 client.delete(wPolicy, key3); このコード実行後のDB内容は以下の通り。 aql> SELECT Name, Release FROM test.NintenGames +------------------+----------+ | Name | Release | +------------------+----------+ | "LegendOfZelda" | 1986 | | "SuperMarioBros" | 1983 | | "Kirby" | 1992 | +------------------+----------+ 3 rows in set (0.111 secs) OK Pockemonが削除された!⭕ 2-4. 複数Record参照 複数Recordを取得したい場合は下記のようにする。 複数Record取得コード // Batchポリシー生成 BatchPolicy bPolicy = new BatchPolicy(); // 参照したいキーを生成 Key[] keys = new Key[3]; keys[0] = new Key("test", "NintenGames", "SMB"); keys[1] = new Key("test", "NintenGames", "LOZ"); keys[2] = new Key("test", "NintenGames", "KBY"); // 複数のRecordを取得 Arrays.stream(client.get(bPolicy, keys)) .forEach(rec -> System.out.printf("Name=[%s] Release=[%d]\n", rec.getString("Name"), rec.getInt("Release"))); Batchポリシーはサーバーへのリクエストを並列マルチスレッドで行う等を指定できるらしい。(デフォルトは1スレッド) このコード実行結果は以下の通り。 実行結果 Name=[SuperMarioBros] Release=[1983] Name=[LegendOfZelda] Release=[1986] Name=[Kirby] Release=[1992] 2-5. 複合操作 1度のクエリで複数の操作を送ることができる。 複合操作コード // ①複合操作対象キー生成 Key key4 = new Key("test", "NintenGames", "SMB"); // ②複合更新Bin Bin binUpdateName = new Bin("Name", "SuperMarioBros2"); Bin binUpdateRelease = new Bin("Release", 3); // ③複合操作実行 Record record2 = client.operate( wPolicy, key4, Operation.put(binUpdateName), Operation.add(binUpdateRelease), Operation.get()); // ④RecordからBinの値を取得 System.out.printf("Name=[%s] Release=[%d]\n", record2.getString("Name"), record2.getInt("Release")); ③の複合操作では、client.operateメソッドを呼び出し、 値の更新(Operation.put) 値のインクリメント(Operation.add) Recordの取得(Operation.get) を一度に実行している。 なお、第一引数のポリシーは書き込みポリシー(WritePolicy)を渡す必要がある。 実行結果は以下のとおり。 実行結果 Name=[SuperMarioBros2] Release=[1986] 実行前DB aql> SELECT Name, Release FROM test.NintenGames +------------------+----------+ | Name | Release | +------------------+----------+ | "LegendOfZelda" | 1986 | | "SuperMarioBros" | 1983 | | "Pockemon" | 1998 | +------------------+----------+ 3 rows in set (0.103 secs) OK 実行後DB aql> SELECT Name, Release FROM test.NintenGames +-------------------+----------+ | Name | Release | +-------------------+----------+ | "LegendOfZelda" | 1986 | | "SuperMarioBros2" | 1986 | | "Pockemon" | 1998 | +-------------------+----------+ 3 rows in set (0.094 secs) OK SuperMarioBrosがSuperMarioBros2になってる!✨ 2-6. 切断 最後にクライアントを切断する。 切断コード client.close(); おわりに 今回はとりあえずここまで。 これでJavaからAerospikeの簡単なCRUD操作はできるようになった。 RecordとBinの関係を理解すれば、 操作は結構シンプルだと思う。 参考 Aerospike Javaクライアント https://docs.aerospike.com/docs/client/java/index.html APIリファレンス https://docs.aerospike.com/apidocs/java/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaのバージョン別、テキストファイルを一括で読み込む方法まとめ

(この記事は 地平線に行く とのマルチポストです) 前回の記事(Javaのバージョン別、1行ずつファイルを読む方法まとめ - Qiita)への感想で、このような話がありました。 丸々読み込む方法のまとめもお願いします!— ユースケ (山本裕介) (@yusuke) May 3, 2021 というわけで、今回はテキストファイルを一括で読み込む方法をまとめました。 (前回と被っている点は省略しているので、まだ読んでない人は先に前回の記事をどうぞ) Java 1.1, 1.2, 1.3 private static String readString(File file) throws IOException { Reader reader = null; try { reader = new InputStreamReader(new FileInputStream(file), "UTF-8"); StringBuffer sb = new StringBuffer(); int len; char[] buffer = new char[1024 * 8]; while ((len = reader.read(buffer)) != -1) { sb.append(buffer, 0, len); } return sb.toString(); } finally { if (reader != null) { reader.close(); } } } まだこのころはめんどくさいです。 FileInputStream でファイルを読み込み、InputStreamReader で指定した文字コードにデコードするようにしています。 読み込む際は、char[] でバッファして StringBuffer に追加していっています。 ちなみに、char[] でバッファしながら読み込んでいるので、 BufferedReader でラップする必要はありません。 Java 1.4, 1.5, 6 private static String readString(File file) throws IOException { CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder(); try (FileInputStream stream = new FileInputStream(file)) { FileChannel channel = stream.getChannel(); ByteBuffer bb = channel.map(MapMode.READ_ONLY, 0, channel.size()); CharBuffer cb = decoder.decode(bb); return cb.toString(); } } ループがなくなって、だいぶ処理がすっきりしました。 このバージョンで追加された Charset クラスから、CharsetDecoder というデコーダを取得できます。 これに FileChannel#map​(FileChannel.MapMode mode, long position, long size) で取得した MappedByteBuffer を渡すことで、ファイルを読み込めます。 MappedByteBuffer はファイルの内容をメモリにマッピングして扱う(メモリマップドファイル)ので、大きなファイルの時に効率的に処理が行われます。 逆に言うと、小さいファイルを読み込む場合は性能が悪いです。 その場合は、今まで通り InputStream で読み込みましょう。 ほとんどのオペレーティング・システムでは、ファイルをメモリーにマッピングするほうが、通常のreadメソッドまたはwriteメソッドを使って数十キロバイトのデータの読み込みまたは書込みを行うよりも負荷が大きくなります。 性能を重視するなら、比較的大きめのファイルだけをマッピングすることをお薦めします。 FileChannel (Java SE 15 & JDK 15) ---- この FileChannel の map メソッドを使うやり方はひしだまさんの記事を読んで知りました! Javaバッファークラスメモ(Hishidama's Java Buffer Memo) Java 7, 8, 9, 10 private static String readString(Path path) throws IOException { byte[] bytes = Files.readAllBytes(path); return new String(bytes, StandardCharsets.UTF_8); } Files クラスが追加され、これに一括でファイルを byte[] として読み込むメソッドが用意されました。 これを使ってファイルを読み込めば、あとは String クラスを new するだけです。 ちなみに、このバージョンでファイルからすべての行を読み取る Files#readAllLines​(Path, Charset) も追加されました。 この戻り値は List<String> です。行ごとに処理をしたい場合はこちらが便利です。 Java 11~ private static String readString(Path path) throws IOException { return Files.readString(path, StandardCharsets.UTF_8); } ひとつのメソッドでファイルをすべて読み込めるようになりました。 1 このメソッドの実装では StringCoding というインターナルなクラスを使っているので、new String(bytes, StandardCharsets.UTF_8) とするよりも効率的な処理になっています。 まとめ めんどくさかった処理も、Java のバージョンが上がるごとに簡単に書けるようになっています。 なので、積極的に新しい Java を使っていきましょう! 関連記事 Javaのバージョン別、1行ずつファイルを読む方法まとめ - Qiita Java の + 演算子による文字列結合は、どのように処理されているのか - Qiita Google で検索して一番上に出てくる JavaDoc が Java 8 なせいで、この Java 11 で追加されたメソッドを忘れられがちです…。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む