20210502のJavaに関する記事は12件です。

Selenium inputかbuttonか分からないボタンをクリック

画面のマニュアルしか持っていなくてHTMLソースをまだ見れないうちに、ボタンクリック動作を書く方法です。ボタンがinputかbuttonか分からないので「|」で結合しています。idやnameがあるかどうかも分からないのでボタン名での記述です。 「|」の前後の半角スペースはあってもなくても動きます。 HTML.html <input type="submit" value="登録" /> または <button type="submit">登録</button> C#.cs driver.FindElement(By.XPath("//input[@value='登録'] | //button[text()='登録']")).Click(); Java.java driver.findElement(By.xpath("//input[@value='登録'] | //button[text()='登録']")).click(); Python.py driver.find_element_by_xpath("//input[@value='登録'] | //button[text()='登録']").click()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開発テスト用にダミーメールアドレスを使用して送受信の確認をする

環境 java8 Spring Boot2.4.0 Eclipse2020-12 目的 受発注システムを開発していて、発注の際に「システムメールを送信して通知する機能」を実装しようとしたけど、ユーザーのメールアドレスはどれもダミーのメールアドレス。動作確認できるように本当は有効なアドレスを使用したほうが良いのかもしれませんが、メールの送信自体はシステムからオンリーになるので、受信の確認はしなくて良い。設定諸々送信側で良いはずなので、テキトーなアドレス宛に「システムからのお知らせです!」みたいなメッセージを送りたい。さあどうする??ってなって迷走していました。 とりあえずメールサーバー付きのレンタルサーバを契約してシステムメールのアカウントは作ったけど、 「ダミーのメールアドレス宛に送ってもエラーだよなぁ・・・」 って思いながら実行すると当然エラー。 そんな折に見つけたのがFakeSMTPでした。 使用したツール FakeSMTP http://nilhcem.com/FakeSMTP/# からダウンロード。 ダウンロード Cドライブ直下に展開して、java -jarで実行。 これでFakeSMTPが立ち上がる。 設定 ホスト名: localhost ポート番号: 25 ポート番号は「リスニングポート」をいじれば変えられるとは思います(自分はやってませんけど、まあできるでしょうね。) ロリポップのSMTPサーバの設定だと、管理画面には465と書いてあるけど、587じゃ無いとうまく送信できませんでした。どういうこと?? TLSとかSSLとかが関係してくるのかもしれませんが、そこまで私はわかりませんm(__)mが、同様のエラーがネットに落ちていてポートをいじればできるとあったので587に変更すれば送信できました。どういう・・・?? 実装 application.properties spring.mail.port=25 spring.mail.host=localhost EmailConfig.java package com.example.home.mail; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import lombok.Data; @Component @Data public class EmailConfig { @Value("${spring.mail.host}") private String host; // ここにプロパティファイルのホストの値が格納される @Value("${spring.mail.port}") private String port; // ここにプロパティファイルのポート番号の値が格納される } MailUtil.java package com.example.home.mail; import java.util.Date; import java.util.Properties; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MailUtil { @Autowired private EmailConfig emailConfig; public void sendEmail(String to) { Properties props = new Properties(); props.setProperty("mail.smtp.host", emailConfig.getHost()); props.setProperty("mail.smtp.port", emailConfig.getPort()); Session session = Session.getInstance(props, new javax.mail.Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(emailConfig.getUsername(), emailConfig.getPassword()); } }); try { // Messageオブジェクト作成 Message msg = new MimeMessage(session); // Fromヘッダーフィールドを設定 msg.setFrom(new InternetAddress(emailConfig.getUsername())); msg.addRecipients(Message.RecipientType.TO, InternetAddress.parse(to, false)); // 件名を設定 msg.setSubject("Javax.Mail test"); // 送信された日付を取得 msg.setSentDate(new Date()); // text/plainのmimeタイプをコンテンツとして設定 msg.setText("MimeMessageのテストです"); Transport.send(msg); } catch (AddressException e) { e.printStackTrace(); } catch (MessagingException e) { e.printStackTrace(); } } これでsendEmailメソッドをどこかで呼び出せばちゃんとFakeSMTPにログが出力されたので、ダミーアドレス宛に送信を試みているのがわかりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JMSでAzure Service Busにメッセージを送信する

Microsoft Learnの以下コンテンツを試したのですが、一部変更が必要な所や足りない所があったので補足記事を書きます。 変更:Azure Spring Boot Starter For Azure Service Bus JMSの依存性 pom.xmlに追加する依存性は現在com.microsoft.azureではなくcom.azure.springのものになっています。 <dependency> <groupId>com.microsoft.azure</groupId> <artifactId>azure-servicebus-jms-spring-boot-starter</artifactId> <version>2.3.3</version> </dependency> ということで以下を追加しました。 <dependency> <groupId>com.azure.spring</groupId> <artifactId>azure-spring-boot-starter-servicebus-jms</artifactId> <version>3.4.0</version> </dependency> 補足:構成パラメータ 「構成パラメーターを追加する」の部分でapplication.propertiesに以下を追加すると書いてあるのですが、知識の足りない私には<xxxxx>に何が入るのかわかりませんでした。 spring.jms.servicebus.connection-string=<xxxxx> spring.jms.servicebus.idle-timeout=20000 ここには接続文字列を入れるようです。作成したService Busの「共有アクセスポリシー」で表示されるプライマリ接続文字列をコピペしました。 spring.jms.servicebus.connection-string=Endpoint=sb://xxxxxxxxxx.servicebus.windows.net/;SharedAccessKeyName=xxxxxxxx;SharedAccessKey=xxxxxxxxx+xxxxx+xxxxxxxx= spring.jms.servicebus.idle-timeout=20000 追加:構成パラメータ application.propertiesにはspring.jms.servicebus.topic-client-idとspring.jms.servicebus.pricing-tierの定義も必要でした。 なかった場合、次のエラーになります。 2021-05-02 10:03:08.497 WARN 53623 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sendController': Unsatisfied dependency expressed through field 'jmsTemplate'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jmsTemplate' defined in class path resource [org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration$JmsTemplateConfiguration.class]: Unsatisfied dependency expressed through method 'jmsTemplate' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jmsConnectionFactory' defined in class path resource [com/azure/spring/autoconfigure/jms/NonPremiumServiceBusJMSAutoConfiguration.class]: Unsatisfied dependency expressed through method 'jmsConnectionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'spring.jms.servicebus-com.azure.spring.autoconfigure.jms.AzureServiceBusJMSProperties': Invocation of init method failed; nested exception is java.lang.NullPointerException 最終的には以下の定義となりました。 spring.jms.servicebus.connection-string=Endpoint=sb://xxxxxxxxxx.servicebus.windows.net/;SharedAccessKeyName=xxxxxxxx;SharedAccessKey=xxxxxxxxx+xxxxx+xxxxxxxx= spring.jms.servicebus.topic-client-id=kikutaroservicebus spring.jms.servicebus.idle-timeout=20000 spring.jms.servicebus.pricing-tier=basic なお、spring.jms.servicebus.topic-client-idはService Busの名前です。 補足:キューの作成 ソースコードはLearnのものをそのまま使いました。 package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class SendController { private static final String queue = "test-queue-jms"; @Autowired private JmsTemplate jmsTemplate; @GetMapping("/messages") public String postMessage(@RequestParam String message) { jmsTemplate.send(queue, s -> s.createTextMessage(message)); return message; } } Learnにはキューを作成する手順がない気がします。。実行すると以下のエラーになりました。 2021-05-02 09:57:00.674 ERROR 52938 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.jms.InvalidDestinationException: The messaging entity 'sb://kikutaroservicebus.servicebus.windows.net/test-queue-jms' could not be found. To know more visit https://aka.ms/sbResourceMgrExceptions. TrackingId:663a3a569bea493fb4f805e6142f5c81_G25, SystemTracker:gateway7, Timestamp:2021-05-02T09:56:59 [condition = amqp:not-found]; nested exception is javax.jms.InvalidDestinationException: The messaging entity 'sb://kikutaroservicebus.servicebus.windows.net/test-queue-jms' could not be found. To know more visit https://aka.ms/sbResourceMgrExceptions. TrackingId:663a3a569bea493fb4f805e6142f5c81_G25, SystemTracker:gateway7, Timestamp:2021-05-02T09:56:59 [condition = amqp:not-found]] with root cause org.apache.qpid.jms.provider.exceptions.ProviderInvalidDestinationException: The messaging entity 'sb://kikutaroservicebus.servicebus.windows.net/test-queue-jms' could not be found. To know more visit https://aka.ms/sbResourceMgrExceptions. TrackingId:663a3a569bea493fb4f805e6142f5c81_G25, SystemTracker:gateway7, Timestamp:2021-05-02T09:56:59 [condition = amqp:not-found] at org.apache.qpid.jms.provider.amqp.AmqpSupport.convertToNonFatalException(AmqpSupport.java:177) ~[qpid-jms-client-0.53.0.jar!/:na] at org.apache.qpid.jms.provider.amqp.builders.AmqpResourceBuilder.getOpenAbortExceptionFromRemote(AmqpResourceBuilder.java:299) ~[qpid-jms-client-0.53.0.jar!/:na] at org.apache.qpid.jms.provider.amqp.builders.AmqpResourceBuilder.handleClosed(AmqpResourceBuilder.java:185) ~[qpid-jms-client-0.53.0.jar!/:na] at org.apache.qpid.jms.provider.amqp.builders.AmqpResourceBuilder.processRemoteClose(AmqpResourceBuilder.java:129) ~[qpid-jms-client-0.53.0.jar!/:na] at org.apache.qpid.jms.provider.amqp.AmqpProvider.processUpdates(AmqpProvider.java:985) ~[qpid-jms-client-0.53.0.jar!/:na] at org.apache.qpid.jms.provider.amqp.AmqpProvider.onData(AmqpProvider.java:871) ~[qpid-jms-client-0.53.0.jar!/:na] at org.apache.qpid.jms.transports.netty.NettyTcpTransport$NettyTcpTransportHandler.channelRead0(NettyTcpTransport.java:563) ~[qpid-jms-client-0.53.0.jar!/:na] at org.apache.qpid.jms.transports.netty.NettyTcpTransport$NettyTcpTransportHandler.channelRead0(NettyTcpTransport.java:556) ~[qpid-jms-client-0.53.0.jar!/:na] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1504) ~[netty-handler-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1253) ~[netty-handler-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1300) ~[netty-handler-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:508) ~[netty-codec-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:447) ~[netty-codec-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276) ~[netty-codec-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:795) ~[netty-transport-native-epoll-4.1.63.Final-linux-x86_64.jar!/:4.1.63.Final] at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480) ~[netty-transport-native-epoll-4.1.63.Final-linux-x86_64.jar!/:4.1.63.Final] at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378) ~[netty-transport-native-epoll-4.1.63.Final-linux-x86_64.jar!/:4.1.63.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989) ~[netty-common-4.1.63.Final.jar!/:4.1.63.Final] at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.63.Final.jar!/:4.1.63.Final] at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na] そこでコードにあるとおり「test-queue-jms」という名前のキューを作成します。 再度実行すると動きました!! キューができているか確認します。 メッセージが届いています。 プログラムで受信する以外に、Service Bus Explorerを使って画面上でメッセージを確認できます。便利。 確認できました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

沈思黙考:Optional(java)

Optional型を使うといわゆるヌルポから解放されるといいますが、どのような場合に使うとよいでしょうか? 参照先の値がnullになる可能性がある、あるいは、nullにならないことがわかっていないケースで、参照先の値をOptional経由で取り出すと、参照先がnullでない場合にだけ値を取り出せるので、Optionalのありがたみがわかります。 たとえば、変数userにユーザー情報が入っていて、user.getCompany()でユーザーの会社情報が取得でき、user.getCompany().getCompanyName()で会社名が取得できるとします。 このとき、user,user.getCompany(),user.getCompany().getCompanyName()のいずれの値もnullになる可能性がある場合に、所属する会社名を出力する処理を、Optionalを使って以下のように書くと、userがnullの場合や、会社が未登録、あるいは、会社名が未登録の場合は、"不明"と表示されます。 sample01 System.out.printf("所属:%s\n",Optional.ofNullable(user) .map(u->u.getCompany()) .map(c->c.getCompanyName()) .orElse("不明")); 慣れてきたらメソッド参照を使って以下のように書くようになるでしょう。 sample01-1 System.out.printf("所属:%s\n",Optional.ofNullable(user) .map(User::getCompany) .map(Company::getCompanyName) .orElse("不明")); もしこれをifをつかって同じことをやると、ここまですっきり書くのは難しいでしょう。3項演算子を使っても冗長になってわかりにくいものになるのも想像できるでしょう。 上記のように値の取り出しを連鎖的に行う場合に、null判定なしに記述できるのが、Optionalの便利な点です。 値の取り出しだけでなく、値の変換を伴う場合もOptionalは便利です。 user.getUserType()でユーザーの社員区分が数値として取り出せて、Map<Integer,String> typeNameMapを参照して区分名称を得るという場合をみてみましょう。getUserType()がnullを返す可能性と、区分がtypeNameMapに存在しない場合があると想定します。typeNameMapに該当する区分があればその区分名を、なければ"区分なし"と表示します。 sample02 System.out.printf("社員区分:%s\n",Optional.ofNullable(user) .map(u->u.getUserType()) .map(t->typeNameMap.get(t)) .orElse("区分なし")); このように処理の結果の値を使って次の値を得るような場合も便利です。 判定を行う場合の例を見てみましょう。 userに趣味を示すgetHobby()があり、これをみて読書が好きかどうか判定する場合を考えます。 次の処理は、getHobby()が"読書"を返す場合はtrueを返し、そうでない場合はfalseを返し、userがnullあるいは、getHobby()がnullの場合はfalseを返します。 sample03 System.out.printf("趣味が読書:%b\n",Optional.ofNullable(user) .map(u->u.getHobby()) .map(h->h.equals("読書")) .orElse(false)); 続いて、趣味がある場合にだけ、読書好きかどうかのフラグを設定する場合を考えましょう。 これもよくある例なのですが、userがnullやgetHobby()がnullの場合はセットをしない点が良い点です。sample03では、userがnullやgetHobby()がnullの場合は、falseが出力されましたので、この違いを確認してください。 sample04 Optional.ofNullable(user) .map(u -> u.getHobby()) .map(h -> h.equals("読書")) .ifPresent(f -> user.setReading(f)); これはメソッド参照を使うと以下のように書けます。 sample04-1 Optional.ofNullable(user) .map(User::getHobby) .map("読書"::equals) .ifPresent(user::setReading); さて、ここまで見てきてOptionalは便利だと感じてもらったと思いますが、なんとなくすっきりしない点があるのを感じませんか? 1つ目は、userやgetCompany()などのどこでnullになったのか、わからない点です。 どの値がnullになることを許容されるのかというのは、設計上重要な問題で、これを明確にしないで実装を行うと、いたるところでヌルポが発生するのは自明です。Optionalを使うことでヌルポがなくなったとしても、設計上の問題を解決したことにはなりません。 Optionalを使うときは、ヌルポを握りつぶしてよいのかどうかを、考えてるとよいでしょう。 2つ目は、orElse()に記載した値です。この値を何にするかを考えることは、重要な設計の一つだと思います。値がnullを返した時にどういうふるまいをすべきか、よく考えるべきです。 適切な値を返すだけでなく、値を返さずに、例外を投げることが必要な場合もあるかと思います。 このようにみると、Optionalを使うことで設計の不備やドキュメントの不備が見えてきます。Optionalは、必要なところで使うと便利であるが、必要以上に使うとバグの発見を先送りすることにつながります。 Optionalを使うところでは、設計を確認するチャンスかもしれません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaのバージョン別、1行ずつファイルを読む方法まとめ

(この記事は 地平線に行く とのマルチポストです) Java でファイル読み込む処理は、バージョンが上がるごとにどんどん簡単に書けるようになっていきました。 今回は、どれだけ簡単になっていったかを Java のバージョンごとにまとめて説明します。 なお、ここでは以下の処理を行うコードをもとにしています。 そこそこ大きいテキストファイルを一行ずつ読み込む 文字コードは UTF-8 Java 1.1, 1.2, 1.3 public static void main(String[] args) throws IOException { File file = new File(args[0]); BufferedReader reader = null; try { reader = new BufferedReader( new InputStreamReader( new FileInputStream(file) , "UTF-8")); String line; while((line = reader.readLine()) != null) { // 処理 } } finally { if (reader != null) { reader.close(); } } } 長いですね。 この時代に FileReader というファイルからテキストを読み込むクラスがあるにはあります。 ただ、このクラスでは問答無用でシステムデフォルトの文字コードが使われてしまいます。 そのため、Windows から Linux に持っていった場合に文字化けが起きる問題がよく起こっていました。 それを避けるためには、文字コードの指定ができる InputStreamReader を使う必要があります。 InputStreamReader 自体ではファイルの読み込みができないので、FileInputStream を使う必要があります。 さらに、一行ずつ読み込むためには BufferedReader を使う必要があります。これ自体はバッファしかしてくれません。 なので、結果として FileInputStream → InputStreamReader → BufferedReader とラップしていく必要があります。 Java 1.4, 1.5, 6 public static void main(String[] args) throws IOException { File file = new File(args[0]); Charset charset = Charset.forName("UTF-8"); BufferedReader reader = null; try { reader = new BufferedReader( new InputStreamReader( new FileInputStream(file), charset)); String line; while ((line = reader.readLine()) != null) { // 処理 } } finally { if (reader != null) { reader.close(); } } } Charset クラスができました。 これにより、文字コードを型で表すことができるようになりました。 ただ、UTF-8 を使う場合に Charset.forName("UTF-8") とする必要がありました。 なので、型安全ではあるけれど、文字列で指定した方が楽という状況でした。 Java 7 public static void main(String[] args) throws IOException { Path path = Paths.get(args[0]); Charset charset = StandardCharsets.UTF_8; try (BufferedReader reader = Files.newBufferedReader(path, charset)){ String line; while ((line = reader.readLine()) != null) { // 処理 } } } 一気にシンプルになりました。 まず、StandardCharsets クラスが追加されました。これにより、わざわざ標準の文字コード1であれば文字列で指定せずに済むようになりました。 また、Files クラスと newBufferedReader(path, charset) メソッドが追加され、いちいちいろんなクラスを new する必要がなくなりました。2 あと、このバージョンで Path クラスが追加されました。 ファイルを読み込むだけど File クラスと大差ないですが、パスの操作が簡単になりました。 さらに、try-with-resources が追加され、めんどくさかった close 処理をいい感じにやってくれるようになりました。 ---- ちなみに…。 このバージョンから まとめて読み込んで List<String> に格納してくれる Files.readAllLines​(Path, Charset) も使えるようになりました。 ただし、これは大きなファイルに使うとメモリを食ってしまうので要注意です。 Java 8, 9, 10 public static void main(String[] args) throws IOException { Path path = Paths.get(args[0]); try (Stream<String> stream = Files.lines(path)){ stream.forEach(line -> /* 処理 */); } } Stream クラスによって、行の読み込みと処理を反復させることができるようになりました。 これのおかげで、1行ずつ処理をしたいという場合にとてもシンプルに書けるようになりました。 また、Files クラスを使う際に、UTF-8 であれば文字コードの指定を省略できるようになりました。 InputStreamReader などは省略時にシステムのデフォルト文字コードが指定されたものとみなされますが、こちらは UTF-8 を指定したものとみなされる という点には要注意です。 ただ、たいていのテキストファイルが UTF-8 であることを踏まえると、妥当な判断かなと思います。 Java 11 ~ public static void main(String[] args) throws IOException { Path path = Path.of(args[0]); try (Stream<String> stream = Files.lines(path)){ stream.forEach(line -> /* 処理 */); } } Path.of​(String first, String... more) というメソッドが追加されました。 内部の処理は Paths.get(String first, String... more) とまったく同じですが、このおかげで Path クラスを使うにはどうすればいいんだっけと迷わずに済むようになりました。 ---- 上記のサンプルでは使っていないのですが…。 なんと、このバージョンで FileReader クラスのコンストラクタで Charset が指定できるようになりました。 Java 1.1 のときに欲しかった…。 なお、InputStreamReader クラスのように文字コードを "UTF-8" のような文字列で指定することはできません。 既存のクラスは互換性のために文字列でも Charset でも指定できるようになっていますが、今後追加されるクラスは Charset のみになるようです。 まとめ Java はファイルを読み込むだけでもなんて面倒くさいんだと言われていました。 でも、それは昔のお話。 今ではとても簡単になっているんだよ、というのが伝われば幸いです。 関連記事 Javaのバージョン別、1行ずつファイルを読む方法まとめ - Qiita Java の + 演算子による文字列結合は、どのように処理されているのか - Qiita US-ASCII, ISO-8859-1, UTF-8, UTF-16BE, UTF-16LE, UTF-16。これらは、どの Java 実装でも必ずサポートされています。 ↩ Files.newBufferedReader の中で、FileInputStream → InputStreamReader → BufferedReader とラップしてくれています。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Effective Java(第3版) 項目5 資源を直接結び付けるよりも依存性注入を選ぶ

インスタンス生成時に、依存する資源(インスタンス)をコンストラクタに渡すことで、 依存性注入(Depenency Injection)を行うべき、という内容です。 記載内容 以前性注入について 名前を知らずに多くのプログラマが使用している。 使用することで、クラスの柔軟性、再利用性、テスト可能性が大幅に向上する Factory Methodパターン パターンの変形として、資源のファクトリを渡すことで、Factory Methodパターンとすることもできる。 Java8で導入されたSupplierは、ファクトリを表現するのに向いている。 フレームワーク プロジェクトの規模が大きい場合、依存性注入によりソースが複雑化してしまう可能性がある。 Dagger、Guice、Spring等のフレームワークを使用することで、 整理することが出来る。 手作業で依存性注入パターンを使用していた場合、フレームワークを容易に導入できる。 考察 Depenency Injectionは、フレームワークにより行うものというイメージが強いかもしれませんが、 コンストラクタにより手動で依存性を注入することでも、多くのメリットが有ります。 特に、テスト自動化を行う上で、処理をモックに差し替えが可能になるのが大きいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Effective Java(第3版) 項目4 privateコンストラクタでインスタンス化不可能を強制する

staticメソッド、staticフィールド(定数)だけからなるクラスを各場合、privateコンストラクタを作成し、 インスタンス化を不可能にする、という内容です。 記載内容 具体的なコンストラクタ定義 以下の様なコンストラクタを定義し、クラスの内部からも含めてインスタンス化が出来ないようにするべき。 Java // インスタンス化できないように、デフォルトコンストラクタを抑制する private UtilityClass() { throw new AssertionError(); } Collectionsの様なインタフェースに対するユーティリティクラス Java8からは、インタフェースにstaticメソッドを定義可能になっているため、 Collectionインタフェースにメソッドを直接定義可能になっている。 考察 privateコンストラクタは、既に一般化されてテクニックとなっており、特に疑問点等は無いと思います。 Java8からは、インタフェースに関するファクトリメソッドは、インタフェース自体に定義するべきと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Selenide + Allureでクリック毎にスクリーンショットを取得して、テストレポートに添付する

やりたいこと Selenideのテスト結果をAllureのテストレポートとして表示させる時に、Selenideでの要素クリック前後のブラウザ画面のスクリーンショットを添付します。 環境 macOS Catalina Maven 3.6.3 JDK 1.8.0_181 下準備 本記事のベースには、Selenideのドキュメントで紹介されているGitHubにあるサンプルプロジェクト selenide-examples/selenide-allure-junit を利用します。 なお、本記事作成時点のSelenideとAllureは、pom.xmlで次のバージョンが指定されていました。 Selenide: 5.18.1 Allure: 2.13.8 まずは、これを適当な場所にダウンロードして動作することを確認します。 READMEに従って、テストを実行し、生成されたレポートがブラウザ上で見られること確認しておきます。 $ curl -L -o selenide-allure-junit-master.zip https://github.com/selenide-examples/selenide-allure-junit/archive/refs/heads/master.zip $ unzip -q selenide-allure-junit-master.zip $ cd selenide-allure-junit-master $ mvn clean test $ mvn allure:serve クリック前後のスクリーンショット取得 src/test/java/org/selenide/examples/に、ScreenshotTest.javaというクラスを作成します。 コード全体は下記です。 ScreenshotTest.java package org.selenide.examples; import com.codeborne.selenide.junit.TextReport; import com.codeborne.selenide.logevents.SelenideLogger; import com.codeborne.selenide.*; import io.qameta.allure.Attachment; import io.qameta.allure.selenide.AllureSelenide; import org.junit.*; import org.openqa.selenium.*; import org.openqa.selenium.support.events.AbstractWebDriverEventListener; import static com.codeborne.selenide.Condition.*; import static com.codeborne.selenide.Selenide.*; import static com.codeborne.selenide.Selectors.*; public class ScreenshotTest { @Rule public TextReport report = new TextReport(); @BeforeClass public static void setUp() { SelenideLogger.addListener("AllureSelenide", new AllureSelenide().screenshots(true).savePageSource(true)); // 匿名クラスを使って実装したイベント補足クラスのインスタンスを登録 WebDriverRunner.addListener(new AbstractWebDriverEventListener() { @Override //要素をクリックする直前の処理 public void beforeClickOn(WebElement element, WebDriver driver){ screenshot(); System.out.println("beforeClickOn:" + driver.getCurrentUrl()); } @Override //要素をクリックした直後の処理 public void afterClickOn(WebElement element, WebDriver driver){ screenshot(); System.out.println("afterClickOn:" + driver.getCurrentUrl()); } @Attachment(type = "image/png") // スクリーンショットを取得 public byte[] screenshot() { return Selenide.screenshot(OutputType.BYTES); } }); } @Test public void searchSelenideTest() { open("https://www.google.co.jp/"); $(By.name("q")).val("Selenide").pressEnter(); $(byText("Selenide: concise UI tests in Java")).click(); $(byText("Blog")).click(); } } WebDriverRunner.addListener()にイベント補足クラスであるAbstractWebDriverEventListenerクラスを匿名クラスを利用して実装したものを登録しています。  参考: イベント処理について beforeClickOn()とafterClickOn()メソッドにそれぞれ要素のクリック直前および直後に行いたい処理を記述します。ここで、スクリーンショットをAllureのテストレポートに添付するscreenshot()というメソッドを呼び出します。 screenshot()メソッドには、Allureの@Attachmentというアノテーションを付与しています。 @Attachmentを使うことでStringまたはbyte[]型のメソッドの戻り値をテストレポートに添付することができます。 @Attachment(type = "image/png") // スクリーンショットを取得 public byte[] screenshot() { return Selenide.screenshot(OutputType.BYTES); } テストレポート ここまで実装したコードを実行し、テストレポートを見てみます。 $ mvn clean test $ mvn allure:serve ブラウザでテストレポートを見ると、クリック前後の画面のスクリーンショットが添付されていることが確認できました。 参考サイト Allure Selenide https://github.com/selenide-examples/selenide-allure-junit Selenide~Javaで超簡単・簡潔にUIテストを書く~
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】MySQLの操作方法(Windows) その2:Javaでの操作

はじめに 前回、DBの準備編にて作成したDBを用いて JavaでのDB操作をやってみたいと思います。 前回の記事 → 【Java】MySQLの操作方法(Windows) その1:DB準備編 前提 動作環境は下記の通りです。 項目 情報 備考 Java バージョン 14.0.2 コマンドプロンプトにて java -version で確認 MySQL バージョン 8.0.24 コマンドプロンプトにて mysql --version で確認 C:\>java -version java version "14.0.2" 2020-07-14 Java(TM) SE Runtime Environment (build 14.0.2+12-46) Java HotSpot(TM) 64-Bit Server VM (build 14.0.2+12-46, mixed mode, sharing) C:\>mysql --version mysql Ver 8.0.24 for Win64 on x86_64 (MySQL Community Server - GPL) 準備 プロジェクトにJDBCドライバーを組み込みます。 ・[プロジェクト] -> [プロパティ] を選択 ・[Javaのビルド・パス] -> [ライブラリ] -> [外部JARの追加] にて、 C:\Program Files (x86)\MySQL\Connector J 8.0\mysql-connector-java-8.0.24.jar を選択し、追加します。 (パスは異なる場合がありますので、ご自身の環境に合わせてください。) 実践(Javaでの操作) 1.1 データベースへ接続 まずは、データベースへ接続してみます。 データベースへ接続するために、java.sql.Connectionのインスタンスを取得します。 今回は、DriverManagerのgetConnection()を使用して取得します。 (この他にも、DIコンテナを使用するなど様々な手法が存在する様子・・) SqlConnect.java import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class SqlConnect { public static void main(String[] args) { // データベース接続で使用する情報 final String jdbcId = "root"; // データベースへ接続するユーザ名 final String jdbcPass = "root"; // パスワード final String jdbcUrl = "jdbc:mysql://localhost:3306/sample"; // ローカル実行なので、localhostを指定。ポートもデフォルトの3306。テスト用のデータベース名のsampleを指定。 // データベースへの接続 try (Connection db = DriverManager.getConnection(jdbcUrl, jdbcId, jdbcPass)) { System.out.println("接続に成功!!"); } catch (SQLException e) { System.out.println("接続に失敗:" + e.getMessage()); } } } データベースへの接続が成功すると、コンソールに「接続に成功!!」と表示されます。 なお、データベースへの接続時のclose処理などの後片付けを自動的にしてくれる点では、try-with-resources構文を使用するのが良いらしい。 参考:try-finallyよりもtry-with-resourcesを使おう しかし、後述のトランザクション処理利用時には、 tryブロックから抜ける際に、catchブロック実行前に自動的にclose処理が動作するため、 明示的なロールバックが記述できないなどのデメリットもある様子・・・ 1.2 データベースの検索 データベース内のデータを検索し、取得します。 SqlQuery.java import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class SqlQuery { public static void main(String[] args) { // データベース接続用の情報 final String jdbcId = "root"; final String jdbcPass = "root"; final String jdbcUrl = "jdbc:mysql://localhost:3306/sample"; try (Connection db = DriverManager.getConnection(jdbcUrl, jdbcId, jdbcPass)) { PreparedStatement ps = db.prepareStatement("select * from members where sex = ?"); ps.setString(1, "女"); ResultSet rs = ps.executeQuery(); while (rs.next()) { System.out.println(rs.getString("name")); System.out.println(rs.getString("sex")); System.out.println(rs.getString("age")); System.out.println("*****************************"); } } catch (SQLException e) { System.out.println("接続に失敗:" + e.getMessage()); } } } まずは、 PreparedStatementを使用して、実行したいSQLのひな型を作成します。 後で値を入れたい部分については、?マークを記述します。(パラメータと呼ばれる) 次に、 ?部分にsetStringを使用して、具体的な値を入れます。 (今回は文字列を入れるので、setStringを使用) setString(パラメータ番号, 文字列)の形。(パラメータ番号は1から始まる。?の位置に対応) 準備ができたので、 SQL文をexecuteQueryを使用して、送信します。 結果はResultSetを使用して受け取ります。 最後に、受け取ったResultSetの中身をwhile文を用いて取り出します。 ResultSetのnextメソッドは戻り値をboolean型で返すので、値がある分だけループで取り出せます。 実行結果は下記の通りとなります。 1.3 データベースへのデータ挿入 INSERT文を用いて、データの登録を行います。 SqlUpdate.java import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; public class SqlUpdate { public static void main(String[] args) { final String jdbcId = "root"; final String jdbcPass = "root"; final String jdbcUrl = "jdbc:mysql://localhost:3306/sample"; String sql = "INSERT INTO members (id, name, sex, age) VALUES (?, ?, ?, ?)"; try (Connection db = DriverManager.getConnection(jdbcUrl, jdbcId, jdbcPass)) { PreparedStatement ps = db.prepareStatement(sql); ps.setString(1, "20210430"); ps.setString(2, "スーパー太郎"); ps.setString(3, "男"); ps.setInt(4, 35); int result = ps.executeUpdate(); System.out.println(result + "行が追加されました。"); } catch (SQLException e) { e.printStackTrace(); } } } スーパー太郎さんを追加しています。 PreparedStatementを使用して、SQLのひな型を準備し、 setString、setIntを使用して、具体的な値を入れ込む部分は検索と同じです。 SQLを送信する際のメソッドが、executeUpdateメソッドになっています。 また、戻り値は「データベースで変更された行数」となるので、int型で受けています。 実行するとこの様な感じになります。 本当に追加されたかは、1.2の検索の条件を変更して実行すれば確認できます。 変更前:ps.setString(1, "女"); 変更後:ps.setString(1, "男"); 実行すると、下記のように追加されている事が確認できます。 1.4 トランザクション処理 最後にトランザクション処理をやってみます。 なお、トランザクション処理そのものの解説は行いませんので、ググって調べてください。 SqlTransact.java import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; public class SqlTransact { public static void main(String[] args) { // データベース接続用の情報 final String jdbcId = "root"; final String jdbcPass = "root"; final String jdbcUrl = "jdbc:mysql://localhost:3306/sample"; String sql1 = "INSERT INTO members (id, name, sex, age) VALUES (?, ?, ?, ?)"; String sql2 = "INSERT INTO members (id, name, sex, age) VALUES (?, ?, ?, ?)"; try (Connection db = DriverManager.getConnection(jdbcUrl, jdbcId, jdbcPass)) { // トランザクション処理のため、自動コミットをオフ db.setAutoCommit(false); try (PreparedStatement ps1 = db.prepareStatement(sql1) ; PreparedStatement ps2 = db.prepareStatement(sql2)) { ps1.setString(1, "20210431"); ps1.setString(2, "田中春香"); ps1.setString(3, "女"); ps1.setInt(4, 20); ps1.executeUpdate(); ps2.setString(1, "20210431"); ps2.setString(2, "鈴木春香"); ps2.setString(3, "女"); ps2.setInt(4, 24); ps2.executeUpdate(); // データベースへコミット db.commit(); } catch (SQLException e) { // データベースをロールバック db.rollback(); System.out.println("処理エラー:" + e.getMessage()); } } catch (Exception e) { e.printStackTrace(); } } } トランザクション処理を行うためには、自動コミットをオフにするために データベース接続に取得したConnection(dbという変数)に対して、 setAutoCommit(false)を実行しています。 そして一連の処理が完了した後に、commit()を呼び出し、コミット処理を実行しています。 なお、途中で処理が失敗した場合には、 処理をキャンセルするために、rollback()を呼び出しています。 今回の例では、idがプライマリキーとなっているにも関わらず、 重複して登録しようとしているので、処理がエラーとなり、誰も追加されません。 (鈴木さんの追加時(ps2のほう)、エラーとなる。) 下記のように変更を行えば、正常に登録がされます。 変更前:ps2.setString(1, "20210431"); 変更後:ps2.setString(1, "20210432"); おわりに Javaを用いて、データベース操作を行ってみました。 今回のやり方以外にも手法が存在するようなので、都度、学習していけたらと思います。 参考:Javaコード入門 参考:スッキリわかるJava入門 実践編 第3版
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Application Insights を Java アプリから使おう

Application Insights を Javaアプリから使おう アプリケーション開発者にとって、実際にアプリがどう動いているかを監視するのは、大事なことだったりします。そのため、例外をハンドルしたり、ログを仕込んだりと色々と準備しておくことも多いでしょう。 今回、ここで紹介する Application Insights ですが、コードに修正を入れることなくアプリケーションの各種メトリクスデータを取得できる優れものです。ログはもちろん、パフォーマンス、例外と言った記録を収集し蓄積してくれます。 公式ドキュメントの入り口は以下です。今では Azure Monitor の一機能的な位置づけになっています。 Azure Application Insights とは何か - Azure Monitor | Microsoft Docs データモデル はじめに Application Insights で収集するデータモデルを理解すると、全体像の把握に繋がると思います。Application Insights が収集するデータは抽象化されており、主要な言語は対応しています。(C#/Java/JavaScript/Python/Node.js) どのようなデータを収集するかといえば、代表的な物は以下の通りです。 データ 意味 リクエスト HTTPのリクエストを記録する トレース アプリケーションのログを記録する 例外 アプリケーションがスローした例外を記録する メトリック Javaの場合だと、JVMの情報が定期的に記録される 依存関係 アプリが依存する外部コンポーネントを記録する。JDBCとか Azure Application Insights Telemetry のデータ モデル - Azure Monitor | Microsoft Docs 例えば、 Java アプリケーションに Application Insights の設定を行っておけば、これらの情報が収集され、Azure 上にある Application Insights に情報が送られ続けます。これらをどのように見るか(可視化)は、ポータルで色々な方法が用意されていますし、クエリ言語でデータを検索することもできます。 Application Insights for Java 現在、2.6系と、3.0系の2つのバージョンがあります。 前者は、アプリケーションにライブラリを含める形でパッケージングする必要がありますので、既存資産に手を入れる必要がります。メンテナンスはされていますが、最新ではないので3.0を使うことをお勧めします。カスタムメトリクスを取得したい場合は、こちらを使う必要もあったりしますが、それは別の機会に。 現時点でのlatestは、3.0系です。 Java エージェントの仕組みを使用しており、既存資産に手を入れずApplication Insights に対応させることができます。 使い方は簡単で、JVM 引数に -javaagent:path/to/applicationinsights-agent-3.0.3.jar と指定するだけです。後で説明しますが、キーの設定は環境変数等に必要ですが。 以下のGithubから最新がダウンロード出来ると思います。執筆時点では、3.0.3が最新で、Java8以降が対象です。 https://github.com/microsoft/ApplicationInsights-Java/releases また、エージェント方式のドキュメントは以下にあります。 Azure Monitor Application Insights Java - Azure Monitor | Microsoft Docs 何が収集されるか 先ほどのドキュメントにもかかれていますが、適当引用しますと、主要のコンポーネントに対応し、それらのメトリクスが自動収集されます。 リクエスト JMS コンシューマー Kafka コンシューマー Netty/WebFlux Servlets Spring スケジュール ログ java.util.logging Log4j (MDC プロパティを含む) SLF4J/Logback (MDC プロパティを含む) 依存関係 Apache HttpClient と HttpAsyncClient java.net.HttpURLConnection JMS Netty クライアント / OkHttp JDBC 実際ためしてみる そこそこのアプリじゃないと、集まるデータもあつまらないので、Spring Pet Clicnc アプリで試してみます。以下から、適当にCloneして環境を整えます。 Application Insights は、Azure ポータルから作成し、インストルメンテーションキーを含む接続文字列を取得しておきます。Application Insights は、Azure 上で動くアプリ限定というわけではありません。オンプレや他プラットフォームからも利用できます。テレメトリの送信はHTTPベースですので、気軽に利用できると思います。 mvn spring-boot:run すると、アプリが起動しますので、ブラウザでアクセスしてみましょう。おなじみの画面が表示されます。 一旦終了し、以下の設定を行います。 pom.xml の maven plugin のJVM引数の構成を設定します。 <configuration> <jvmArguments>-javaagent:c:\yourpath\applicationinsights-agent-3.0.3.jar</jvmArguments> </configuration> 次に接続文字列を環境変数に設定します。 set APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=... 観察してみる 再度実行して、Webを操作してみましょう。しばらくすると、送信されたテレメトリをAzure ポータル側で見られるようになります。代表的なものをいくつか見てみます。 ライブメトリクス 簡単に動作しているか確認するには、ライブメトリクスを開きます。リアルタイムにアプリケーションの状況が確認できます。言語によってはサポートしてない場合もあった気もしますが、Java の 3.0系ではサポートされています。 検索 ざっくりどんなものが記録されているか確認するには、トランザクションの検索を見てみます。下記の画像のように、Request/Dependency テレメトリが記録されていることがわかります。 アプリケーションマップ 自アプリケーションと、外部コンポーネントの依存関係がマップ形式表示されます。Spring Pet Clinicですと、外部コンポーネントが内部のDBくらいしかないので、この程度の表示になってしまいますが、ストレージに依存していたり、CosmosDbに依存したり、外部のAPIに依存していれば、当然それらはマップ化されます。 失敗 例外が記録されます。ハンドルされないものや、例外オブジェクト付のログなんかは、ここに記録されます。Spring Pet Clinic では、例外を起こす画面があるので、そのページをアクセスしてみましょう。 この例ではあまり有意な情報はでませんが、エラーの上位や、エラーをドリルダウンしていくことができます。 詳細 ドリルダウンしていくと以下のような詳細画面を表示できます。どのリクエストから、どの外部コンポーネントが呼ばれているのかを、チャート形式で見ることが出来ます。例外基点でどのようなリクエストがエラーを引き起こしたかなどの原因究明に役立つでしょう。 ログの検索 記録された生のデータは、ログの検索から見ることが出来ます。テレメトリ毎にKQLというクエリ言語で検索したり、それらをグラフ化したりできます。以下はリクエストを取得した例です。 余談ではありますが、ログを検索すれば、どのようなSQLが発行されたかを確認したり、 VMの統計情報を見ることも出来ます。 まとめ 今回は、Azure Application Insights を試してみましたが、アプリケーション開発者にとって入れて損することはないので、是非設定しましょう。WebApps/Functions などは、ポータルから設定することもできるので、ほぼなにもすることはありません( すでに、3.0 ベースのエージェントになっていると思いますが、未確認) 一定量までは無料で使うことも出来ますし、色々試してみることをお勧めします。ある程度データが蓄積されてないと面白くないのですが。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BASE64URL Encodingのすすめ

あのURLで見たBASE64の名前を僕達はまだ知らない。 あれは BASE64URL Encoding と言うらしい。 (どれだよ) Base64#変形版 - Wikipedia URLアプリケーションのための変形 Base64 ('base64url' encoding) 概要 BASE64 でエンコードした文字列には、 URL 上で意味を持つ + 、 / 、 = が含まれている。そのため URL で利用するためには工夫が必要である。BASE64 には BASE64URL Encoding と言う変形版が公式仕様にあり、URL Encoding や Percent Encoding よりも、URL上で利用するには都合が良い様に感じた。 今回WEBアプリで BASE64URL Encoding を扱うにあたり、ついでに BASE64 の自体も実装をしてみようと思い、やってみた、そんな話。 BASE64の解説 ① BASE64 (original) 読んでみると超シンプルな代物。 Base64 - Wikipedia バイナリ全体をbitの配列 (bit stream) として捉え、6bit ずつ区切り、対応する文字列に置き換える。 6bit = 2^6 = 64 (⇒ 64文字種) 文字テーブル: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ (64文字種) 3btyte (24bit) ずつ処理し、4byteに伸長する。※ 24は8と6の最小公倍数。 不足するbitは0で埋める。 不足するbyteは = 文字で埋める。 必ず文字列長は4の倍数になる。 コーディングの前に視覚化して整理するのも良いと思う。 ② BASE64 URL Encoding (URL安全なBASE64) BASE64 の仕様 RFC 4648 内で Section 5 「Base 64 Encoding with URL and Filename Safe Alphabet」 にて定義されている。 ・RFC 4648 - The Base16, Base32, and Base64 Data Encodings #Section-5 Base 64 Encoding with URL and Filename Safe Alphabet + 、 / 、 = は URL 上意味を持つ記号のため、それらの文字を回避したもの。 (URL上の役割は割愛。) 62番 + ⇒ - で表現する。 63番 / ⇒ _ で表現する。 = によるパディングは行わない。 結構シンプルで、ほぼ一緒。 # bit hex BASE64 BASE64URL # bit hex BASE64 BASE64URL 0 00000000 0x00 A A 32 00100000 0x20 g g 1 00000001 0x01 B B 33 00100001 0x21 h h 2 00000010 0x02 C C 34 00100010 0x22 i i 3 00000011 0x03 D D 35 00100011 0x23 j j 4 00000100 0x04 E E 36 00100100 0x24 k k 5 00000101 0x05 F F 37 00100101 0x25 l l 6 00000110 0x06 G G 38 00100110 0x26 m m 7 00000111 0x07 H H 39 00100111 0x27 n n 8 00001000 0x08 I I 40 00101000 0x28 o o 9 00001001 0x09 J J 41 00101001 0x29 p p 10 00001010 0x0A K K 42 00101010 0x2A q q 11 00001011 0x0B L L 43 00101011 0x2B r r 12 00001100 0x0C M M 44 00101100 0x2C s s 13 00001101 0x0D N N 45 00101101 0x2D t t 14 00001110 0x0E O O 46 00101110 0x2E u u 15 00001111 0x0F P P 47 00101111 0x2F v v 16 00010000 0x10 Q Q 48 00110000 0x30 w w 17 00010001 0x11 R R 49 00110001 0x31 x x 18 00010010 0x12 S S 50 00110010 0x32 y y 19 00010011 0x13 T T 51 00110011 0x33 z z 20 00010100 0x14 U U 52 00110100 0x34 0 0 21 00010101 0x15 V V 53 00110101 0x35 1 1 22 00010110 0x16 W W 54 00110110 0x36 2 2 23 00010111 0x17 X X 55 00110111 0x37 3 3 24 00011000 0x18 Y Y 56 00111000 0x38 4 4 25 00011001 0x19 Z Z 57 00111001 0x39 5 5 26 00011010 0x1A a a 58 00111010 0x3A 6 6 27 00011011 0x1B b b 59 00111011 0x3B 7 7 28 00011100 0x1C c c 60 00111100 0x3C 8 8 29 00011101 0x1D d d 61 00111101 0x3D 9 9 30 00011110 0x1E e e 62 00111110 0x3E + - 31 00011111 0x1F f f 63 00111111 0x3F / _ pad n/a n/a = n/a 実装 ① BASE64 ⇔ BASE64URLの相互変換 BASE64 Safe URL Convert.(Java) | Online editor and compiler 変換.java /** * URL安全なBASE64に変換. * * @param base64 * @return base64url */ private static String convertBase64SafeUrl(final String base64) { if(base64 == null) { return null; } else { String ret = base64; ret = ret.replaceAll("\\+", "-"); ret = ret.replaceAll("\\/", "_"); ret = ret.replaceAll("=+$", ""); return ret; } } /** * 通常 (URL非安全) のBASE64に変換. * * @param base64url * @return base64 */ private static String convertBase64UnSafeUrl(final String base64url) { if(base64url == null) { return null; } else { String ret = base64url; ret = ret.replaceAll("-", "+"); ret = ret.replaceAll("_", "/"); ret += "==="; ret = ret.substring(0, ((ret.length() / 4) * 4)); return ret; } } 変換結果 url safe :8KCut/CfpJTwn42G8J+RjQ== (24) >>>> 8KCut_CfpJTwn42G8J-RjQ (22) url unsafe :8KCut_CfpJTwn42G8J-RjQ (22) >>>> 8KCut/CfpJTwn42G8J+RjQ== (24) url safe :01234=== (8) >>>> 01234 (5) url safe :012345== (8) >>>> 012345 (6) url safe :0123456= (8) >>>> 0123456 (7) url safe :01234567 (8) >>>> 01234567 (8) url safe :a+/b==a+/b++a+/b//a+/b==a+/b==a+/b== (36) >>>> a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34) url safe :a+/b==a+/b++a+/b//a+/b==a+/b==a+/b (34) >>>> a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34) url unsafe :01234 (5) >>>> 01234=== (8) url unsafe :012345 (6) >>>> 012345== (8) url unsafe :0123456 (7) >>>> 0123456= (8) url unsafe :01234567 (8) >>>> 01234567 (8) url unsafe :012345678 (9) >>>> 012345678=== (12) url unsafe :a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34) >>>> a+/b==a+/b++a+/b//a+/b==a+/b==a+/b== (36) url unsafe :a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34) >>>> a+/b==a+/b++a+/b//a+/b==a+/b==a+/b== (36) url unsafe :?*+/-_=== (9) >>>> ?*+/+/====== (12) 文字列途中にある = については、そもそものルール違反のため、関知しない。 BASE64の文字についても、そもそものルール違反のため、関知しない。 元々末尾に = がついていた場合も、関知せずパディングを行う。 ② BASE64のEncode/Decode STEP① とりあえずBYTE配列をBASE64にするコードを作ってみる。 とりあえず、書いてみた。 6bitごとにbyte配列を整形する 処理と 文字列化するコード を分けて記述。 進数変換と同様の考え方。今回のbit を bit mask で抽出し、前回余ったbit とを論理和で合成する。 今回余ったbit は 左シフトし、不用bitを落としておき、次回に持ち越す。 今回取り出すbitの右シフト量 ( v ) は 2, 4, 6 の3パターン。 (⇒ (((i % 3) + 1) * 2) ) 次回に持ち越すbitの左シフト量は右シフト量 ( v ) に応じて求められ、 4, 2, 0 の3パターン。 (⇒ (6 - v) ) 上位2bitは落としておく。 左シフトのため下位bitは無視して良い。 6bitごとにbyte配列を整形するメソッド public static byte[] convert6bitBinary(byte[] data) { if(data == null) { return data; } else { byte mod = 0x00; List<byte> list = new List<byte>(); byte[] lowMaskList = new byte[] { 0x3F, // [0] 00111111 0x0F, // [1] 00001111 0x03, // [2] 00000011 }; for(int i = 0; i < data.Length; i++) { int j = i % 3; int v = ((j + 1) * 2); byte lowMask = lowMaskList[j]; byte b0 = data[i]; byte b6 = (byte)(mod | (lowMask & (b0 >> v))); list.Add(b6); mod = (byte)(0x3F & (b0 << (6 - v))); if(j == 2) { list.Add(mod); mod = 0; } } if((data.Length % 3) != 0) { list.Add(mod); } return list.ToArray(); } } 文字列化するコード public static string ToBase64(byte[] data, bool isUrlSafe = false) { StringBuilder sb = new StringBuilder(); if(data != null) { string base64_map; if(isUrlSafe) { base64_map = BASE64_MAP_URLSAFE; } else { base64_map = BASE64_MAP_BASIC; } foreach(byte b in data) { sb.Append(base64_map[b]); } if(!isUrlSafe) { int max; sb.Append("==="); max = (sb.Length / 4) * 4; sb.Remove(max, sb.Length - max); } } return sb.ToString(); } STEP② 4文字ごと に着目した処理を考え実装する。 BASE64は比較的古くからある仕様であり、展開や実装の容易さと言うのも兼ね備えていたと考える。なので、 = を用いたパディング は何のために必要なのか疑問語を覚えるが。これは逆説的に、4文字ごと に作業を行うことで、最適化が行える を可能性を感じられる。特に 4文字 で表現される 24 bit は 8 と 6 の 最小公倍数 である。即ち、3byte ずつ 4文字 にする のである。この程度であれば、下手にベタ書きしたした方が可読性もあり、 if文 も減り、高速化が望めそうである。そんな訳で、処理を見直して、Encode処理とDecode処理を組み直してみる。 ※ 現在のPCのCPUは計算機の発展であり、計算が得意で条件分岐が苦手である。レガシーなPCでは限られたリソースで速度を得るため、条件分岐を省き効率よく処理を作る必要性が高かった。 Encode (BASE64化) 3byteずつ、 int配列[3] にコピーする。byteの長さを超えた場合は、9bit目だけが立っている 0x0100 にする。 int配列[3] から byte配列[4] 6bitずつ格納していく。 (1) int配列[n]の9bit目 を byte配列[n]の7bit目 に論理和。 ※ 6bitに伸長した場合は7-8bit目は使わないので、存在しないバイトはbitを立てて64以上の値にする。 (2) 上位ビットはカレントとして int配列[n]の該当bit を byte配列[n - 1]の該当bit へ 左シフト + マスク (抽出) して 論理和 (3) 下位ビットは不足分として int配列[n - 1]の該当bit を byte配列[n - 1]の該当bit へ 右シフト + マスク (抽出) して 論理和 存在するバイト値は 0 ~ 63 の値になる。 ※ 0x0100 の下位8bitが0なので、不足分で使われても問題ない。 存在しないバイト値は 64 の値になる。 ※ これも、0x0100 の効果。 byte配列 の値を文字化して格納。 通常の BASE64 では変換表の 64番目 に = を入れておき、条件分岐無くパディング行う。 URL安全の BASE64 URL 場合は、64番目 以上はスキップします。 encode処理抜粋 private static readonly string BASE64_MAP_BASIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; private static readonly string BASE64_MAP_URLSAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; public static string encode(byte[] data, bool isUrlSafe = false) { StringBuilder sb = new StringBuilder(); for(int i = 0; i < data.Length; i += 3) { int[] src = new int[3]; byte[] tgt = new byte[4]; //copy or default. for(int j = 0; j < 3; j++) { int k = i + j; if(k < data.Length) { src[j] = data[k]; } else { //(bit: 0001 0000) src[j] = 0x0100; } } // reformat. tgt[0] = (byte)((0x00) | (0x00) | (0x3F & (src[0] >> 2))); tgt[1] = (byte)((0x00) | (0x3F & (src[0] << 4)) | (0x0F & (src[1] >> 4))); tgt[2] = (byte)((0x40 & (src[1] >> 2)) | (0x3F & (src[1] << 2)) | (0x03 & (src[2] >> 6))); tgt[3] = (byte)((0x40 & (src[2] >> 2)) | (0x3F & (src[2] << 0)) | (0x00)); // to character. if(isUrlSafe) { foreach(byte b in tgt) { if(b < 0x40) { sb.Append(BASE64_MAP_URLSAFE[b]); } } } else { sb.Append(BASE64_MAP_BASIC[tgt[0]]); sb.Append(BASE64_MAP_BASIC[tgt[1]]); sb.Append(BASE64_MAP_BASIC[tgt[2]]); sb.Append(BASE64_MAP_BASIC[tgt[3]]); } } return sb.ToString(); } 結構条件分岐が減って、良い感じ。また、符号化には最低でも2文字必要になるため、1-2文字目用のパディング判定処理は省いている。 Decode (BASE64からの複合) 通常の BASE64 と URL安全の BASE64 URL どちらでも複合ができる様にしている。 文字列 から byte配列[4] に値を格納する。存在しないbyte値は、64 にする。 通常の BASE64 では変換表の 64番目 に = を入れておき、こちらも 64 になる。 値が見つからなかった時は、URL安全の BASE64 URL の変換表も探索し、- _ を補完する。 byte配列[4] から int配列[3] へ 8bitずつ格納していく。 (1) byte配列[n + 1]の7bit目 を int配列[n]の9bit目 に論理和。 (2) 上位ビットはカレントとして byte配列[n]の該当bit を int配列[n]の該当bit へ 左シフト + マスク (抽出) して 論理和 (3) 下位ビットは不足分として byte配列[n + 1]の該当bit を int配列[n]の該当bit へ 右シフト + マスク (抽出) して 論理和 存在するバイト値の場合 byteの範囲を内の 0 ~ 255 の値になり、バイナリに追加する。 存在しないバイト値の場合、byteの範囲を超えた 256 (0x0100) の値になり、バイナリに追加しない。 decode処理抜粋 private static readonly string BASE64_MAP_BASIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; private static readonly string BASE64_MAP_URLSAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; public static byte[] decode(string base64) { List<byte> list = new List<byte>(); if(!string.IsNullOrEmpty(base64)) { for(int i = 0; i < base64.Length; i +=4) { byte[] src = new byte[4]; int[] tgt = new int[3]; for(int j = 0; j < 4; j++) { int k = i + j; byte code; if(k < base64.Length) { char c = base64[k]; int index = BASE64_MAP_BASIC.IndexOf(c); if(index < 0) { index = BASE64_MAP_URLSAFE.IndexOf(c); } code = (byte)index; } else { // 01000000 (64) code = 0x40; } src[j] = code; } tgt[0] = (0x00) | (0xFC & (src[0] << 2)) | (0x03 & (src[1] >> 4)); tgt[1] = (0xFF00 & (src[2] << 2)) | (0xF0 & (src[1] << 4)) | (0x0F & (src[2] >> 2)); tgt[2] = (0xFF00 & (src[3] << 2)) | (0xC0 & (src[2] << 6)) | (0x3F & (src[3] >> 0)); foreach(int val in tgt) { if(val < 0xFF) { list.Add((byte)val); } else { break; } } } } return list.ToArray(); } こちらも変換表の64番目に = を追加して64以上を使うことで、処理を少し簡略化出来ている感がある。 また、複合には最低でも2文字必要になるため、1byte目の複合には存在チェック用のbit処理を省いている。 本来 = 前の文字の場合は更にその前の余りを含んでいるので、 1文字の場合 ⇒ BASE64上ありえない (= で3文字パディング。) 2文字の場合 ⇒ = の 前の文字の下位4bitが0 (= で2文字パディング。) 3文字の場合 ⇒ = の 前の文字の下位2bitが0 (= で1文字パディング。) 4文字の場合 ⇒ 余りなし (= でパディングする必要もなし。) な感じになる。 実行例 BASE64 scrap (C#) | Online editor and compiler 実行例抜粋 // check strs[0]------------------------------------------------------- #source str : Hellow C# bin (hex) : 48 65 6C 6C 6F 77 20 43 23 bin (bit) : 01001000 01100101 01101100 01101100 01101111 01110111 00100000 01000011 00100011 #encode base64 : SGVsbG93IEMj base64url : SGVsbG93IEMj #decode (basic) SGVsbG93IEMj bin (hex) : 48 65 6C 6C 6F 77 20 43 23 bin (bit) : 01001000 01100101 01101100 01101100 01101111 01110111 00100000 01000011 00100011 str : Hellow C# #decode (urlsafe) SGVsbG93IEMj bin (hex) : 48 65 6C 6C 6F 77 20 43 23 bin (bit) : 01001000 01100101 01101100 01101100 01101111 01110111 00100000 01000011 00100011 str : Hellow C# // check strs[1]------------------------------------------------------- #source str : BASE64- bin (hex) : 42 41 53 45 36 34 2D bin (bit) : 01000010 01000001 01010011 01000101 00110110 00110100 00101101 #encode base64 : QkFTRTY0LQ== base64url : QkFTRTY0LQ #decode (basic) QkFTRTY0LQ== bin (hex) : 42 41 53 45 36 34 2D bin (bit) : 01000010 01000001 01010011 01000101 00110110 00110100 00101101 str : BASE64- #decode (urlsafe) QkFTRTY0LQ bin (hex) : 42 41 53 45 36 34 2D bin (bit) : 01000010 01000001 01010011 01000101 00110110 00110100 00101101 str : BASE64- // check strs[2]------------------------------------------------------- #source str : ???? bin (hex) : F0 A0 AE B7 F0 9F A4 94 F0 9F 8D 86 F0 9F 91 8D bin (bit) : 11110000 10100000 10101110 10110111 11110000 10011111 10100100 10010100 11110000 10011111 10001101 10000110 11110000 10011111 10010001 10001101 #encode base64 : 8KCut/CfpJTwn42G8J+RjQ== base64url : 8KCut_CfpJTwn42G8J-RjQ #decode (basic) 8KCut/CfpJTwn42G8J+RjQ== bin (hex) : F0 A0 AE B7 F0 9F A4 94 F0 9F 8D 86 F0 9F 91 8D bin (bit) : 11110000 10100000 10101110 10110111 11110000 10011111 10100100 10010100 11110000 10011111 10001101 10000110 11110000 10011111 10010001 10001101 str : ???? #decode (urlsafe) 8KCut_CfpJTwn42G8J-RjQ bin (hex) : F0 A0 AE B7 F0 9F A4 94 F0 9F 8D 86 F0 9F 91 8D bin (bit) : 11110000 10100000 10101110 10110111 11110000 10011111 10100100 10010100 11110000 10011111 10001101 10000110 11110000 10011111 10010001 10001101 str : ???? // check 1st impl. encode : 6A 05 39 => agU5 encode : 6A 05 => agU= encode : 6A => ag== encode : 6A 05 39 => agU5 encode : 6A 05 => agU encode : 6A => ag decode : agU5 => 6A 05 39 decode : agU= => 6A 05 decode : ag== => 6A decode : a=== => decode : agU5 => 6A 05 39 decode : agU => 6A 05 decode : ag => 6A decode : a => // check 2nd impl. encode : 6A 05 39 => agU5 encode : 6A 05 => agU= encode : 6A => ag== encode : 6A 05 39 => agU5 encode : 6A 05 => agU encode : 6A => ag decode : agU5 => 6A 05 39 decode : agU= => 6A 05 decode : ag== => 6A decode : a=== => decode : agU5 => 6A 05 39 decode : agU => 6A 05 decode : ag => 6A decode : a => BASE64URL の活用法 私が今回使おうと思った背景では、Servlet のWEBアプリケーションで ChromeのAcrobat拡張 を利用するために、リクエストをURLに再現性のない POST から再現性のある GET に変更しようと思ったのがある。パラメータは複数あり、あまり生で見せたくないというのもあり、全パラメータをまとめて BASE64 での シリアライズ を行うことにした。 ChromeのAcrobat拡張 ではURLを再度開いているだけのため、再現性のあるURLである必要がある。これはfirefoxで内蔵ビューアで開いたPDFをオンザフライでAdobeReaderなどに開かせる際も同様かと思う。 GETによるアプリケーション連携が増えている昨今。一時的なリクエストにせよ、GET化することのメリットがそれなりに出てきている。 と言うか、私は パーマネント リンク大好きマン なので参照メインのサイトで パーマネント リンク 取れない時に結構キレてます。えぇ、最近のアニメ公式サイトとか多いですね。。。すっごいゴテゴテで、友人にshareしようとしたら、 パーマネント リンク が取れなくてTOPページしか送れないこととか。。。 ダイミダラーの公式サイト を見習って欲しい (そういう意味ではFLASHが死に絶えたのはほんと、良かったのかも知れない。FLASHだけで作ってるサイトも度々あったので) パーセント エンコーディング や URI エンコーディング じゃダメだったのか? パーセント エンコーディング 及び URI エンコーディングはどんな文字列にもエンコード/デコードを処理をかけることができ、符号化/復号化がブラウザやWEBサーバーのリクエスト処理に含まれてしまっている。そのため、間に認証ページなどでジャンプが挟まる様な場合、ジャンプ数を加味して多重でパーセント エンコーディングをかけたりする必要がある場合がある。特に = が含まれている場合には、多段でジャンプ中にQueryStringとして前のページでのパラメータとして喰われてしまい、自分のページに届かない、なんてこともある。BASE64 の場合は + / により、多段ジャンプ自体が阻害される場合もある。 BASE64URL では自分で展開処理を行わなければいけないデメリット自体がそのままメリットとなり、他のページとの競合を避けることができる。誰も勝手に複合したりしないので、最後までそのまま自分のところに届く。 多段ジャンプ(笑) と思うこともあるかも知れないが。案外Twittterの認証など色々画面見てても、結構パラメーターを引きずっていることが多くあると思い、そんなにマイノリティでは無いかと思う。 HTTPのリクエストのセキュリティは、GET 以上のものが POST で得られるもんでも無いと思うので、もっと別の形で確保してください。 (だから認証でも GET を使っているのでしょう。) 総括 ベンチマークは取らないが、BASE64の実装をして理解を深めることが出来たと思います。とてもシンプルなロジックなので、一度自分で書いてみるのは、bit演算の訓練にもなって良いかと思った。あと、行数によってはベタに書くことも悪く無く、今回の BASE64 もその類なのかと思う。 なお、JavaとC#が混在しているのは単に気分です。( 当方、final教信者 の C#er です )。 参考 (謝辞) Base64 - Wikipedia https://ja.wikipedia.org/wiki/Base64 RFC 4648 - The Base16, Base32, and Base64 Data Encodings https://tools.ietf.org/html/rfc4648
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

@PreparedForTestに入るクラス、お前やったんかい(PowerMockでwhenNewを伴う場合)

PowerMockでハマったので備忘。 クラス内処理で毎回インスタンスが生成されるようなクラスをテストしたい場合、生成されるインスタンス内の処理をモック化(つまり、テスト用に理想的なIN/OUTを設定)するには、PowerMockを用いる必要があります。 この際、ソースは下記のようになります ※下記の記事のソースにコメントを追記したものです テスト対象クラス public class ExampleService { public Integer count() { ExampleLogic logic = new ExampleLogic(); //テストに当たって、countExamplesの処理はどうでもいいのでモック化したい return logic.countExamples(); } } テストクラス/テストメソッド import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; import static org.powermock.api.mockito.PowerMockito.when; import static org.powermock.api.mockito.PowerMockito.whenNew; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @RunWith(PowerMockRunner.class) //hiroがハマったポイント、PreparedForTest @PrepareForTest({ ExampleService.class }) public class ExampleServiceTest { ExampleService service = new ExampleService(); @Test public void count() throws Exception { ExampleLogic logic = mock(ExampleLogic.class); whenNew(ExampleLogic.class).withNoArguments().thenReturn(logic); when(logic.countExamples()).thenReturn(0); assertThat(service.count(), equalTo(0)); } } hiroがハマったのはPreparedForTestの中身にどのクラスを書くか、でした。 正解 //テスト対象のクラスを書くのが正解 @PrepareForTest({ ExampleService.class }) 不正解 //モック化されるクラスを書くと不正解 @PrepareForTest({ ExamleLogic.class }) 不正解の方を書くと、うまくモック化されず実際の処理が行われてしまいます。 今後気を付けていきます!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む