JavaMailを使ってメール送受信をおこなう方法

Java

今回は、JavaMailを使ってメールの受信、および送信おこなう方法を紹介します。


JavaMailとは、Javaに標準実装されているメール送信APIです。
簡単な実装で、メールの受信と送信をおこなうことができます。

今回は、Yahooメールを使用した実装サンプルを紹介します。


環境情報


環境の情報は以下になります。

今回は、Yahooメールを使ってメールの送受信をおこなってみます。

メールはYahooのフリーアカウントを使用します。

  • Java1.8.60
  • POP3:Yahooメール
  • SMTP:Yahooメール

Yahooメールを受信する


メール受信プログラムは以下になります。
YahooメールのPOP3サーバに接続し、受信ボックスに存在するメールを取得して標準出力をおこなっています。
22行目のIDとパスワードは、自分のYahooメールのIDをパスワードを使用してください。


import java.util.Properties;

import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Folder;
import javax.mail.Message;

public class MailReceive {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        try {
            // メール受信のプロパティ設定
            Properties props = new Properties();
            props.put("mail.pop3.host", "pop.mail.yahoo.co.jp");
            props.put("mail.pop3.port", "110");

            // メール受信フォルダをオープン
            Session session = Session.getDefaultInstance(props);
            Store store = session.getStore("pop3");
            store.connect("ID", "PW");
            Folder folderInbox = store.getFolder("INBOX");
            folderInbox.open(Folder.READ_ONLY);

            // メッセージ一覧を取得
            Message[] arrayMessages = folderInbox.getMessages();
            for (int lc = 0; lc < arrayMessages.length; lc++) {

                // メッセージの取得
                Message message = arrayMessages[lc];

                // 件名の取得と表示
                String subject = message.getSubject();
                System.out.print("件名:" + subject + "\r\n");

                // 本文の取得と表示
                String content = message.getContent().toString();
                System.out.print("本文:" + content + "\r\n");

                // 取得の最大件数は10件
                if (lc >= 10) {
                    break;
                }
                System.out.print("\r\n");
            }
        } catch (Exception e) {
            System.out.print("例外が発生!");
            e.printStackTrace();
        } finally {
        }
        System.out.print("end: main\r\n");
    }
}

Javaメールのメール受信をおこなえるようにするためには、Yahooメールでの設定変更が必要です。
Yahooメールの設定画面で、POPサーバについて、Yahoo公式サービス以外からのアクセスを許可します。
送信の場合はSMTPを使うので、POPとSMTPについては許可設定をしておく必要があります。


Yahooメール設定1
Yahooメール設定2

Yahooメールを送信する


次にメール送信です。
メールの送信元がYahooメールで、メールの送信先もYahooメールにしています。
自分自身にメールを送っています。


IDとパスワード、メールアドレスについては、実際に送信する情報にあわせて修正が必要になります。


import java.util.Properties;

import javax.mail.Address;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.PasswordAuthentication;
import javax.mail.Transport;
import javax.mail.Multipart;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

public class MailSend {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        try {
            // メール送信のプロパティ設定
            Properties props = new Properties();
            props.put("mail.smtp.host", "smtp.mail.yahoo.co.jp");
            props.put("mail.smtp.port", "587");
            props.put("mail.smtp.auth", "true");
            props.put("mail.transport.protocol", "smtp");
            props.put("mail.smtp.ssl.trust", "*");
            props.put("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.connectiontimeout", "10000");
            props.put("mail.smtp.timeout", "10000");

            // セッションを作成する
            Session session = Session.getInstance(props,
                new javax.mail.Authenticator() {
                    protected 
                        PasswordAuthentication 
                        getPasswordAuthentication() {
                        return new 
                            PasswordAuthentication("ID", "PW");
                    }
                });

            // メールの送信先はYahooメール。送信元もYahooメール
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress(
                "fromAddress", "fromName"));
            message.setReplyTo(new Address[]{
                new InternetAddress("toAddress")});
            message.setRecipients(Message.RecipientType.TO, 
                InternetAddress.parse("toAddress"));
            message.setSubject("テスト");
            MimeBodyPart messageBodyPart = new MimeBodyPart();
            messageBodyPart.setText("テストメール。");

            // メールのメタ情報を作成
            Multipart multipart = new MimeMultipart();
            multipart.addBodyPart(messageBodyPart);
            message.setHeader(
                "Content-Transfer-Encoding", "base64");

            // メールを送信する
            message.setContent(multipart);
            Transport.send(message);

        } catch (Exception e) {
            System.out.print("例外が発生!\r\n");
            e.printStackTrace();
        } finally {
        }
        System.out.print("end: main\r\n");
    }
}

筆者がちょっとはまったのが、以下の例外が発生した場合です。


javax.mail.NoSuchProviderException: No provider for smtp
        at javax.mail.Session.getProvider(Session.java:464)
        at javax.mail.Session.getTransport(Session.java:659)
        at javax.mail.Session.getTransport(Session.java:640)
        at javax.mail.Session.getTransport(Session.java:697)
        at javax.mail.Transport.send0(Transport.java:192)
        at javax.mail.Transport.send(Transport.java:124)
        at MailSend.main(MailSend.java:55)

“smtpのプロバイダーが存在しないよ”、と言われてしまっています。


この例外が発生する場合は、実行時のクラスパスに「mail.jar」が含まれていないときです。
「mail.jar」がない状態でビルドしてもビルドエラーにならないので、気が付きにくいかもしれません。


最後に


いかがでしたでしょうか?
Javaメールを使った、メール受信とメール送信の方法について紹介しました。


今回はYahooメールを使用したメール送受信方法を紹介しましたが、他メールサービスでも試してみたいと思います。(いつか。。。)
それではまた!




XSLファイルを使って、SpringMVCでCSV出力をおこなう方法

SpringFrameWork

WEBアプリケーションにおいて、サーバでCSVを作成して、クライアント(ブラウザ)にダウンロードする、といった要件は頻繁に発生します。


SpringMVCでXSLファイルを上手に使えば、簡単にCSVファイルの作成とダウンロードの機能を作成することができます。


今回は、SpringMVCでCSV出力をおこなう方法を紹介します。
Javaで自力でゴリゴリ作成する方法ではなく、XSLファイルを使ってCSV出力おこなう方法となります。


XSLファイルとは、XMLファイルのスタイルを定義しているファイルになります。
出力するCSVファイルのレイアウトをXSLファイルに定義してあらかじめサーバ上に格納しておき、CSVファイルを出力する際に使用します。


XSLファイルとは何か?


XSL(eXtensible Style Language)ファイルは、XML 文書を他の文書タイプに変換したり、出力をフォーマットする際に使用できるスタイルシートです。
Styleという言葉が使われていることからもわかるように、XMLのスタイルシートがXSLになります。


このXSLファイルで便利なのが、テンプレート関数というちょっとしたロジックをXSLファイル内で使用することができる点です。
例えば、以下のような関数です。


  • テンプレート関数の定義の仕方(xsl:template、xsl:param)
  • 引数の値の参照の仕方($引数名)
  • 条件分けの仕方(xsl:if、xsl:choose)
  • XSLT関数(not、contains、substring-before、substring-after)
  • XSLT関数の使い方(<xsl:value-of select=”XSLT関数 (引数…)” />)
  • 定義されたテンプレート関数の呼び出し方(xsl:call-template、xsl:with-param:再帰呼び出しの箇所)

上記のような関数もあらかじめXSLファイル内に定義することができます。


以下に、よく使う関数を紹介していきます。


文字列の連結


concat(str1,str2,str3,・・・)
str1、str2、str3、を連結


文字列の調査


contains(str, substr)
strの中のsubstrを検索。
存在する場合はtrueを通知


数字のフォーマッティング


format-number(number, format)
format-number(number, format, formatType)
numberの数字を、formatで指定されたフォーマットに変換して出力します。


空白の除去


normalize-space(str)
strから空白を除外した文字列を返却します


先頭文字のチェック


starts-with(str, substr)
strがsubstrで始まっているかをチェックします。
始まっている場合はtrueを通知。


文字列への変換


string(val)
valを文字列に変換します。


文字列の長さを取得


string-length(str)
strの長友を取得します。


文字列の抜き出し


substring(str, start)
substring(str, start, length)
strに対して、start位置からlengthの長さの文字列を取得します。


文字列の抜き出し(前文字列の取得)


substring-before(str, substr)
strからsubstrの文字列を検索し、見つかったらその前の文字列を取得します。


substring-after(後文字列の取得)


substring-after(str, substr)
strからsubstrの文字列を検索し、見つかったらその後の文字列を取得します。


文字列の置換


translate(str, src, dest)
strに含まれるsrcという文字列をdestに変換します。


切り上げ


ceiling(val)
valの切り上げをおこないます。


ノード数の取得


count(node)
XML内に含まれるnodeで指定されたノードの数を取得します。

<a>
  <b></b>
  <b></b>
</a>

この例で、’b’のノード数は2です。


切り下げ


floor(val)
valの切り下げをおこないます。


数値に変換


number(any)
anyを数値に変換します。


四捨五入


round(val)
valを四捨五入します。


加算


sum(node)
nodeで指定されている値を加算し、数値で返却します。


<a>
    <b>10</b>
    <b>10</b>
</a>

sum(‘b’)の返却値は20です。


現在のノード取得


current()
現在位置のノードを取得します。


現在のノードの最下層ノードを取得


last()


nodeのローカル名(名前空間を外したもの)を取得


local-name(node)


現在のノード名を取得


name()


現在のノードの名前空間URIを取得


namespace-uri()


現在のノード位置を数値で取得


position()


boolean型への変換


boolean(any)
anyをboolean(true or false)に変換します。


falseの出力


false()
boolean型のfalseを出力します。


trueの出力


true()
boolean型のtrueを出力します。


否定


not(boolean)
booleanの否定値をbooleanで出力します。


ドキュメントルートの取得


document(uri)
uriのドキュメントルートを出力します。


xsltで解釈できるxlstノードかを判定


element-available(str)
返り値はbooleanです。


xsltで解釈できる関数かを判定


function-available(func)


ノードに固有の値を割り当て


generate-id(node)


該当するidのノードを取得


id(any)


キー値を返却


key(str any)
str内のハッシュに、anyで指定される表現を返却します。


XSLT固有値の取得


system-property(str)
str内のXSLT固有の値を取得します。


<!entity>タグ値を取得


unparsed-entity-uri()
<!entity>タグで指定されている値を取得します。


CSV出力のサンプルソース


CSV出力のサンプルソースを紹介します。


SpringMVCのコントローラクラスに実装することを想定しています。


@RequestMapping(params = "csvOut", produces = "text/csv")
public ModelAndView makeCsvOut(HttpServletResponse response, Form form)
        throws JAXBException, IOException {

    // DBから出力する情報を取得してDTOに格納
    CsvOutDTO dto = this.service.geCsvInfo(form);

    // DTOをXMLの形式に変換
    JAXBContext context = JAXBContext.newInstance(CsvOutDTO.class);
    Writer writer = new StringWriter();
    context.createMarshaller().marshal(dto, writer);

    // モデルに変換したXMLを格納
    String xslName = "CsxXsl";
    ModelAndView modelAndView = new ModelAndView(xslName);
    modelAndView.addObject(
        "xmlSource", new StringReader(writer.toString()));

    // HTTPレスポンスヘッダを作成
    response.setHeader("Content-Disposition", 
        "attachment; filename=csvFile.csv, "UTF-8"));

    // BOMを先頭に付加
    response.getOutputStream().write(CxFix.BOM);

    return modelAndView;
}

データベースからCSV出力する内容を取得することを想定しています。
「geCsvInfo」の中身は消略していますが、このメソッドの中身がDBアクセスしてDTOのインスタンスを返却するイメージです。


その後はサンプルの通りで、DTOを「JAXBContext」に渡せば、CSVファイルの出来上がり


次にXSLファイルのサンプルです。

上記のJavaソースで”CsvXsl”という名前で定義されたXSLファイルのサンプルになります。


<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns="http://www.w3.org/1999/xhtml">
    <xsl:output method="text" omit-xml-declaration="yes" 
        byte-order-mark="yes" encoding="UTF-8" />
  <xsl:template match="/csvList">
    <xsl:call-template name="template_csvList" />
  </xsl:template>
    <xsl:template name="template_csvList">
        <!-- ヘッダ情報 -->
        <xsl:text>カラム1,</xsl:text>
        <xsl:text>カラム2,</xsl:text>
        <xsl:text>カラム3,</xsl:text>
        <xsl:text>
    </xsl:text>
        <!-- データ情報 -->
        <xsl:value-of
            select="translate(translate(translate(
                ./entity/col1, '"', ' '), ',', ' '), '', ' ')" />
        <xsl:text>,</xsl:text>
        <xsl:value-of
            select="translate(translate(translate(
                ./entity/col2, '"', ' '), ',', ' '), '', ' ')" />
        <xsl:text>,</xsl:text>
        <xsl:value-of
            select="translate(
                translate(translate(./entity/col3, '"', ' '), 
                ',', ' '), '', ' ')" />
        <xsl:text>,</xsl:text>
    </xsl:template>
</xsl:stylesheet>

「entity」とは、前のJavaコードに記載していた、「CsvOutDTO」で使用しているエンティティクラスです。
1行データ毎に1エンティティのレコードを保持します。


CSVファイルは1行目は各項目名の説明で、実際のデータ出力は2行目からです。
XSLファイルで2行目以降を動的にすることで、「CsvOutDTO」に格納されているエンティティの数だけCSVデータ出力がおこなわれます


関数は「translate」しか使っていません。
ダブルクオテーションと改行コードを半角スペースに変換しています。


まとめ


いかがでしたでしょうか?


SpringMVCでCSV出力をおこなう場合に、参考にしてもらえればと思います。
特に、XSLの関数は助かります。
ちょっとした処理をJavaでおこなう必要がないので。


それではまた!


Javaで動画変換をおこなう。JAVE(Java Audio Video Encoder)でMP4に!

Javeでの画像変換

今回は、Javaで動画変換をおこなう方法を紹介します。
使用するライブラリは『JAVE』です。



あんまりメジャーではないライブラリのようですね。
でも、使い方がシンプルで、かつ、世の中に出回っているほとんどの動画ファイル形式に対応しているので、筆者は気に入っています。


具体的には、『mp4』に変換することを前提にサンプルプログラムを紹介していきます。


mp4に変更するのは特に理由はないですが、あえて理由をあげるとすれば、”圧縮率が一番よさそうだから”です。


検証環境


筆者が動作検証した環境は以下になります。


  • Windows7
  • Java1.8.06
  • JAVE-1.0.2

JAVE(Java Audio Video Encoder)について


JAVE(Java Audio Video Encoder)とは何か?ということになるのですが、公式サイトの説明を日本語化すると以下になります。


JAVE(Java Audio Video Encoder)ライブラリは、ffmpegプロジェクトのJavaラッパーです。
開発者は、JAVEを利用して、オーディオファイルとビデオファイルをある形式から別の形式にトランスコードできます。
例では、AVIファイルをMPEGファイルにトランスコードでき、DivXビデオストリームを(YouTubeのような)Flash FLVファイルに変更でき、WAVオーディオファイルをMP3またはOgg Vorbisに変換できます。
オーディオトラックとビデオトラックをトランスコードする場合、ビデオのサイズを変更したり、サイズや比率を変更したりできます。
JAVEでは、他の多くの形式、コンテナ、および操作がサポートされています。


動画操作で有名なライブラリは「ffmpeg」ですが、JAVEはffmpegをラッピングしているライブラリみたいです。


説明文をみるとように、様々なファイル形式に対応しています。
ベースはffmpegなので、当然っちゃ当然ですね。


JAVEでMP4に変換する


一通りの動画について変換を試してみました。
一覧でまとめました。
動画は基本的に圧縮されます。


拡張子

mp4変換時の圧縮率

.avi

70%

.mp4

70%

.m2ts

99%

.ts

27%

.mpeg

27%

.mpg

29%

.mkv

29%

.wmv

41%

.webm

42%

.ogm

180%

.mov

96%


JAVEを使用した動画変換処理


動画変換処理のサンプルコードを紹介します。
使い方は簡単で、変換処理の条件(コーデック、ビットレート、など)を属性情報として設定し、変換処理を実行します。
様々な形式に変換が可能(っぽい)ですが、MP4形式への変換サンプルコードになります。


import it.sauronsoftware.jave.AudioAttributes;
import it.sauronsoftware.jave.Encoder;
import it.sauronsoftware.jave.EncoderException;
import it.sauronsoftware.jave.EncodingAttributes;
import it.sauronsoftware.jave.InputFormatException;
import it.sauronsoftware.jave.VideoAttributes;
import it.sauronsoftware.jave.VideoSize;

import java.io.File;

public class JaveManage {

    private JaveManage() {
    }

    public static void main(String[] args) {
        System.out.println("start.");

        try {
            // 動画フォーマットを設定
            EncodingAttributes attrs = 
                new EncodingAttributes();

			// 動画圧縮変換時フォーマット
            attrs.setFormat("mp4");

            // 音声の変換情報を設定
            AudioAttributes audio = new AudioAttributes();
			// 動画圧縮変換時オーディオコーデック
            audio.setCodec("aac");
			// 動画圧縮変換時オーディオビットレート
            audio.setBitRate(256000);
			// 動画圧縮変換時オーディオチャンネル
            audio.setChannels(1);
            audio.setSamplingRate(88200);
			// 動画圧縮変換時オーディオサンプリングレート
            attrs.setAudioAttributes(audio);

            // 動画の変換情報を設定
            VideoAttributes video = new VideoAttributes();
			// 動画圧縮変換時ビデオコーデック
            video.setCodec("libx264");
			// 動画圧縮変換時ビデオビットレート
            video.setBitRate(2700000);
			// 動画圧縮変換時ビデオフレームレート
            video.setFrameRate(30);
			// 動画圧縮変換時ビデオ幅,動画圧縮変換時ビデオ高さ
            video.setSize(new VideoSize(1296, 768));
            attrs.setVideoAttributes(video);

            // 変換を実施
            File source = new File("source.wav");
            File dest = new File("source.mp4");
            Encoder encoder = new Encoder();
            encoder.encode(source, dest, attrs);

            // 変換結果を検証
            if (!dest.exists() || dest.length() == 0) {
                System.out.println("encode failer.");
            }
        } catch (EncoderException e) {
            System.out.println("Occured EncoderException.");
        } catch (Exception e) {
            System.out.println("Occured Exception.");
        }
        System.out.println("end.");
    }
}

設定値はソースコードに直接記述していますが、以下のパラメータを設定します。


  • オーディオビットレート:256000
  • オーディオチャンネル:1
  • サンプリングレート:88200
  • ビデオビットレート:2700000
  • フレームレート:30
  • サイズ:height:1296 width:768

まとめ


いかがでしたでしょうか?


JAVEはあんまりメジャーなライブラリではないようですね。
ググってもあんまり情報がないです。


使ってみた直観としては、使いやすい印象です。
「パラメータを設定して変換」っていうだけですからね。


皆さんが使う際に、参考にして頂ければと思います。


それではまた!



SpringMVCでトランザクション管理をおこなう。アノテーションとXMLの設定

SpringFrameWork

SpringMVCでは、アノテーションの設定をおこなうことだけでDBトランザクションの管理をおこなうことができます。


今回は、SpringMVCでトランザクション管理をおこなう方法について説明していきます


SpringMVCでは、トランザクションのスタート位置をアノテーションで指定することにより、トランザクションの開始を定義することができます
トランザクションのスタート位置を定義することにより、どのDB処理を一つのトランザクションとするかをコントロールすることができるということになります。


しかし、こういった制御をおこなうためには、アノテーションの準備とXMLへの設定が必要になります。


トランザクションとは


トランザクションとは何でしょうか?
ここでのトランザクションとは、“データベースのトランザクション”になります。
直訳すると『取引』ですね。


データベース処理では、1回のSQL発行ではデータの整合性が取れない場合が多々あります。
例えば、あるデータをシステムに登録しようとした場合、複数のテーブルにレコードの登録と更新が必要な場合。
テーブルが3つあり、あるデータをシステムに登録しようとした場合、テーブルAにはレコードの登録、テーブルBにはレコードの更新、テーブルCにもレコードの更新が必要だとします。
この場合、テーブルBのレコード更新が失敗したとすると、テーブルAのレコード登録は無かったことにしたいです。
具体的には”コミットしたくない”。


コミットは、全てのDB処理が完了してから(テーブルCへのレコード更新)おこないたいです。


トランザクションの管理

各テーブルへの処理を終わったタイミングでコミットしてしまうと、途中でエラーが発生した場合に、データの整合が取れていない状態でデータができあがってしまいます。
これを避けるために、3つの処理をまとめて考え、3つの処理が全て完了してからコミットします。
途中で、エラー等が発生した場合はロールバックして元に戻します。


この、複数のDB処理を一つにまとめることをトランザクションと呼びます。


どの処理をまとめて一つのトランザクションにするかを考えるのは、安定したシステムを作るのにきちんと考えておきたいポイントです。


common-database-context.xmlの定義


SpringMVCでトランザクション定義をおこなう場合は、「common-database-context.xml」への定義が必要になります。


まずはトランザクション管理するための基本定義です。
トランザクション管理するためのクラス定義と、DBコネクションのデータソース設定です。


「DataSourceTransactionManager」を使用可能な状態にし、「DataSourceTransactionManager」が使用するデータソースを定義します。


<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
    <property name="defaultTimeout" value="10" />
</bean>

プロパティ値として、タイムアウト値も定義します。
10秒です。


通常、SpringMVCで開発したアプリケーションは、Tomcat等のWEBアプリケーション上に配置することになります。
WEBアプリケーションが生成したコネクションプール内のDBコネクションを、SpringMVCが取得してアプリケーションが使用するのが通常になります。


次に、トランザクションを有効にするパッケージの定義とアノテーションの定義。
以下のように定義します。


<aop:config proxy-target-class="false">
    <aop:advisor advice-ref="txAdvice" pointcut="execution(public * jp.co.xxx..*Controller.*(..)) && @annotation(jp.co.xxx.annotation.DbTransaction)" />
</aop:config>

「pointcut」に、トランザクション管理対象のパッケージを定義します。
パッケージを「execution」括ります。


かつ、「@annotation」に、トランザクション開始を意味するアノテーション定義を記載します。
「jp.co.xxx.annotation.DbTransaction」が、アノテーションのJavaクラスになります。


トランザクション管理するアノテーション設定


ここまで準備した「common-database-context.xml」への定義とアノテーション定義で、トランザクション管理をおこなうことが可能になりました。
アノテーションを定義したメソッドがトランザクションの開始位置で、そのメソッドが完了したら自動的にコミットされます。
仮に、メソッドの途中で例外が発生してメソッドが中断された場合はロールバックされます。(コミットされません)
定義としては以下になります。


@DbTransaction
    public void xxx() {

通常は上記でトランザクション管理ができるのですが、個別にトランザクションの開始やコミット・ロールバックをおこないたい場合があります。
独自のトランザクション管理についても可能です。
以下のように、自分でイベントをプログラミングすればよいです。


// トランザクション管理を読み込み
@Autowired(required = false)
private PlatformTransactionManager transactionManager;

public void xxx() {

    // 処理開始
    TransactionStatus status = startTransaction();

        ・
        ・
        ・

    if (isDone) {
        // コミット
        transactionManager.commit(status);
    } else {
        // ロールバック
        transactionManager.rollback(status);
    }
}

まとめ


いかがでしたでしょうか?


SpringMVCでは、アノテーションの準備をするだけでトランザクション管理をおこなうことが可能になります。


システムの方針によるかと思うのですが、トランザクションの単位をどれにするかもいろいろです。
1つのコントローラメソッドについて1トランザクションにするか、サービスのメソッドについて1トランザクションにするか、
といった悩みどころもあります。


システムでルールを統一した上で、SpringMVCで開発を進めていくのがいいかと思います。


それではまた!



SpringMVCでアノテーションを使ったり、自分で作ってみたりする

SpringFrameWork

皆さん、普段は普通にアノテーションを使ってコーディングしているかと思います。
筆者は、アノテーションについて、Java開発初期時は特に重要に考えていませんでした。

ただの、JavaDocに記述するためのキーワードぐらい?程度です。


でも、アノテーションにロジックを組み込めることを知ってから、自作アノテーションは重宝しています。


今回は、アノテーションを自分で作成する方法を紹介していきます。


大規模なシステムとなり開発メンバが増えれば増えるほど、処理の統一の意味であらかじめ準備されているアノテーションではなく、システム固有のアノテーションを作りたくなってきます。
自分でアノテーションを作って、使用する処理のヘッダに組み込んでいく、といった形です。


今回は、自前でアノテーションを作る方法を紹介していきます。


アノテーションとは


アノテーションには、大きく分けて以下の3つがあります。


  • 名前だけでデータのないマーカー 例:@Override、@Deprecated
  • データをひとつ持つ単一アノテーション 例:@SuppressWarnings
  • 複数のデータをもつフルアノテーション

アノテーションは、メソッドのコメントに「@(アットマーク)」を付けることで有効になります。
当然ながら、ここに書いたアノテーションはJavaDocに出力されます。


日本語としては『注釈』です。
『注釈』の説明としては以下になります。


  • 語句や文章の意味をわかりやすく解説すること。また、それをした文。 「古典を-する」 「 -を加える」
  • 補足的な説明。

そもそもアノテーションを記載する意味ですが、要は、”複数人でプログラムを開発する時に、コーディングのやり方を統一するため”と言ってよいかと思います。


複数人で開発を同時進行する場合、コーディング規約を定めていたとしても、どうしても人によってそれぞれのプログラムが出来上がってしまうことがあります。


その、人による差をゼロにすることはできないですが、なるべく差を小さくしていくための仕組みの一つがアノテーションです。


例えば、「@Override」。
「@Override」を付ける意味は、”このメソッドは親メソッドのオーバーライドメソッドだよ。”という『注釈』です。
このアノテーションを付けた場合、親メソッドに同メソッドが存在しない場合はエラーになります。


開発チーム内で、こういったルールを設けていれば、勝手にオーバーライドメソッドを作る確率が減ります。
※あくまで、”確率が減る”だけですが。


代表的なアノテーション


SpringMVCであらかじめ準備されているアノテーションについて紹介しておきます。
代表的なやつ、です。


@Autowired

 

自動注入(Dependency Injection)する際に使用する

 

@Autowired

 

自動注入(Dependency Injection)する際に使用する

 

@Component

 

自動注入(Dependency Injection)して使いたいクラスに付与

 

@Service

 

サービスクラスに付与する

 

@Transactional

 

トランザクション境界を定義する際に付与

 

@RestController

 

APIのコントローラに付与

 

@Controller

 

ページのコントローラに付与

 

@Validated

 

バリデーションを自動的に実施

 

@Min

 

最少値チェック

 

@Max

 

最大値チェック

 

@Size

 

桁数チェック

 

@Pattern

 

正規表現チェック

 

 


アノテーションを自分で作る


それではアノテーションを自分で作ってみましょう。
アノテーションを作るには、以下の3つを準備すればよいです。


アノテーションの定義


「@interface」をつけて、アノテーション定義をおこないます。
ここでは、「anotationAccess」というアノテーションを定義しています。


public class anotationControl {
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface anotationAccess {
    }
}

アノテーションのロジック部


アノテーションが実行された際のロジックを記述するクラスを準備します。
ここでは、「anotationLogic」というメソッドを準備しています。


public class anotationLogic {

    private anotationLogic() {
    }

    public void anotationAccessLogic() {

        ※アノテーションの中身のロジック。ここに書く。

    }
}

定義とロジックの紐付け


アノテーションの定義とロジックの紐つけをおこないます。
定義は「servlet-context.xml」におこないます


ここまで作成した定義とロジックであれば、以下のような紐つけになります。


<aop:config>
  <aop:aspect id="ana" ref="anotationAccess">
    <aop:pointcut id="anaPointcut" expression="@annotation(jp.co.xxx.anotationLogic)"/>
      <aop:before pointcut-ref="racPointcut" method="anotationAccessLogic" />
    </aop:aspect>
</aop:config>

「jp.co.xxx.anotationLogic」は、システム独自のパッケージになります。
こうすることで、システム内で自作のアノテーションが使用できるようになります。


アノテーションの使い方は、あらかじめ準備されているアノテーションを使う場合とまったく同じです。


@anotationAccess
public xxx method() {

まとめ


いかがでしたでしょうか?
アノテーションを使ったり、自分で作る際の参考にしてもらえればと思います。


それではまた!



SpringSecurityを使ったセッション制御。同時ログインを許可しない

SpringSecurity

今回は、SpringSecurityを使っての同時ログイン制御について説明します。


SpringSecurityを使った場合、セッションの作成や制御はSpringSecurityが自動でやってくれますが、「applicationSecurity.xml」で然るべき定義をおこなえば、同時ログイン制御も簡単におこなうことができます。



同時ログイン制御は、システムによって仕様が異なるかと思います。
同じユーザIDで複数ユーザでのログインを許可しているシステムもあれば、同じユーザIDで複数ユーザでのログインを許可しないシステムもあります。


こういった制御は、自前で制御を作るのではなく、「applicationSecurity.xml」の定義で制御可能です。


SpringSecurityの基本的な使い方


SpringSecurityの基本的な使い方については、以下の記事を参照してください。



SpringSecurityを使えば、ログイン機能があるシステムで必要な以下の制御が可能になります。


  • ログイン認証
  • セッション管理
  • 不正アクセスに対する制御
  • ワンタイムトークン

同時ログイン制御で必要な「applicationSecurity.xml」の定義


基本的な「applicationSecurity.xml」の定義は以下になります。
同時ログインを許可していないシステムでの定義例になります。


<bean id="sessionStrategy" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy" >
  <constructor-arg index="0">
    <list>
      <bean class="jp.co.xxx.MyConcurrentSessionControlAuthenticationStrategy">
        <constructor-arg index="0" ref="sessionRegistry" />
        <property name="maximumSessions" value="1" />
        <property name="exceptionIfMaximumExceeded" value="false"/>
      </bean>
    </list>
  </constructor-arg>
</bean>

同時ログインを許可しないために必要なプロパティ設定について解説していきます。


最大セッション数 – maximumSessions


1ユーザが許可する最大セッション数になります。
ここを1にすると、1ユーザが許可する最大セッション数は1になるので、同時ログインは許可しないということになります。


例えば、この値に「100」を定義すると、同じユーザIDでログインを許可する最大セッション数は100になります。
つまり、同時ログインを100セッションまで許可する、ということになります。


セッション数が最大を超えた場合に例外を発生させる – exceptionIfMaximumExceeded


「maximumSessions」の数を超えた場合、例外を発生させるかどうか?の定義になります。
trueだと例外を発生させ、falseだと例外を発生させない。


上記の「applicationSecurity.xml」の定義では、例外は発生させたくないのでfalseにしています。


Javaで制御をおこなう – ConcurrentSessionControlAuthenticationStrategy


XMLに定義されていますが、「MyConcurrentSessionControlAuthenticationStrategy」で個別制御をおこなうことができます。
「MyConcurrentSessionControlAuthenticationStrategy」は「org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy」
を継承したクラスになります。


具体的には、以下のクラスを準備して個別制御をおこないます。


import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;

public class MyConcurrentSessionControlAuthenticationStrategy extends
        ConcurrentSessionControlAuthenticationStrategy {

    private SessionRegistry mySessionRegistry;

    public MyConcurrentSessionControlAuthenticationStrategy(
            SessionRegistry sessionRegistry) {
        super(sessionRegistry);
        this.mySessionRegistry = sessionRegistry;
    }

    @Override
    public void onAuthentication(Authentication authentication,
            HttpServletRequest request, HttpServletResponse response) {

        //-- 個別処理をこの中に書く --//

    }
}

「onAuthentication」は、認証が成功した場合に実行されるメソッドになります。
個別処理が必要な場面とは以下になります。
全て、同時ログインを許可しない「applicationSecurity.xml」の定義をおこなった場合の制御です。


  • ある条件に一致するユーザは、同時ログイン制御を許可させる。
  • 『後勝ち』の制御をおこなう。

『後勝ち』の制御はスペシャルですね。
既にログインしているユーザのセッションを削除する処理が必要になります。


以下のような実装が必要になります。


// 同一ユーザでのセッションリストを取得
List<SessionInformation> sessions = this.mySessionRegistry
    .getAllSessions(authentication.getPrincipal(), false);

 // 許可するセッションの数を取得
int allowedNum = getMaximumSessionsForThisUser(authentication);

// セッションリストのうち、ログインしたユーザだけを有効にし、
// 他セッションを無効にする
allowableSessionsExceeded(sessions, allowedNum, this.mySessionRegistry);

まとめ


いかがでしたでしょうか?


SpringSecurityで同時ログイン制御が簡単におこなうことができることがわかって頂けたかと思います。


「applicationSecurity.xml」の定義だけではなく、SpringSecurityは必ずスペシャルな処理を個別に実装することが可能です。
「applicationSecurity.xml」の定義だけでやり切る(やり切れる)部分と、スペシャルに実装する部分を見極めて実装することが大事かと思います。


それではまた!



JavaでSocket通信をおこなう。サーバとクライアントのサンプルコード。

javaソケット

Javaでソケット通信を、自前で開発しなければいけない事はあるかと思います。
ソケットを待ち受ける側が常駐プロセスとして確立させ、ソケットの送信を常に待ち受けている状態。
ソケットを待ち受ける側がサーバとなり、ソケットを送る側がクライアント側になりますね。


今回は、ソケット通信のサーバ側とクライアント側のサンプルソースコードを紹介いたします。
ソケットの送受信のみのサンプルコードになりますので、システム固有の処理については別途組み込んで使ってください。


環境情報


今回のサンプルコードのビルド環境、実行環境は以下になります。


  • Java12.0.1

Javaの標準ライブラリしか使っていないです。
Apache等の拡張ライブラリ(JAR)は必要なし、になります。


サンプルプログラムの機能


サーバのクラス名は「SocketReceive」
クライアントのクラス名は「SocketSend」


クライアントは、クライアント上に存在するファイルである「client_send.txt」をストリーム上に読み込みます。
クライアントの「SocketSend」は、サーバの「SocketReceive」に接続して「client_send.txt」の内容を送信します。
「SocketReceive」は受信した内容を「server_recv.txt」にファイル出力します。


「SocketReceive」は、ファイル出力が終わった後にまったく逆のことをおこないます。
「SocketReceive」は、「server_send.txt」をストリーム上に読み込み「SocketSend」に送信します。
「SocketSend」は受信した内容を「cient_rect.txt」に出力します。


送受信プログラムの概要

ソケット通信プログラム

クライアントプログラム – ソケット送信


クライアントプログラム(送信側)のサンプルは以下になります。
上の説明での「SocketSend」になります。


import java.sql.SQLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.io.IOException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class SocketSend {

    private SocketSend() {
    }

    public static void main(String[] args) {

        Socket socket = null;
        FileOutputStream fos = null;
        FileInputStream fis = null;
        try {
            System.out.println("start.");

            // 出力ソケットを作成
            socket = new Socket("localhost", 8100);

            // 入出力ストリームを準備
            fis = new FileInputStream("client_send.txt");
            fos = new FileOutputStream("client_recv.txt");

            // 入力ストリームの内容を全て送信
            int ch;
            OutputStream output = socket.getOutputStream();
            while ((ch = fis.read()) != -1) {
                output.write(ch);
            }
            output.write(0);

            // サーバからのレスポンスを受信し、ファイルに出力
            InputStream input = socket.getInputStream();
            while ((ch = input.read()) != -1) {
                fos.write(ch);
            }
        } catch (SocketException e) {
            System.out.println("occured SocketException.");
        } catch (Exception e) {
            System.out.println("occured Exception.");
        } finally {
            try {
                socket.close();
                fos.close();
                fis.close();
            } catch (IOException e) {
                System.out.println("occured IOException.");
            }
            System.out.println("end.");
        }
    }
}

サーバプログラム – ソケット受信


次はサーバプログラム(受信側)のサンプルです。
上の説明での「SocketReceive」になります。


import java.sql.SQLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.io.IOException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class SocketReceive {

    private SocketReceive() {
    }

    public static void main(String[] args) {

        Socket socket = null;
        FileOutputStream fos = null;
        FileInputStream fis = null;
        try {
            System.out.println("start.");

            // 入出力ストリームを準備する
            fos = new FileOutputStream("server_recv.txt");
            fis = new FileInputStream("server_send.txt");

            // 入力ソケットを作成して、送信を待ち受ける
            ServerSocket server = new ServerSocket(8100);
            socket = server.accept();

            // ソケットを受信する
            int ch;
            InputStream input = socket.getInputStream();
            while ((ch = input.read()) != 0) {
                fos.write(ch);
            }

            // 受信した内容をファイル出力
            OutputStream output = socket.getOutputStream();
            while ((ch = fis.read()) != -1) {
                output.write(ch);
            }
        } catch (SocketException e) {
            System.out.println("occured SocketException.");
        } catch (IOException e) {
            System.out.println("occured IOException.");
        } catch (Exception e) {
            System.out.println("occured Exception.");
        } finally {
            try {
                socket.close();
                fos.close();
                fis.close();
            } catch (IOException e) {
                System.out.println("occured IOException.");
            }
            System.out.println("end.");
        }
    }
}

まとめ


いかがでしたでしょうか?
ソケット通信の仕組みを開発する必要が出てきた場合は参考にしてください。


ちょっとできなかったのは、「SocketReceive」の常駐プロセス化。
現在のサンプルプログラムは1回受信したら終わってしまうので。


ここら辺はアレンジして使ってください。


それではまた!




SpringSecurityで認証機能を実現する!セキュアなWEBサイトへの第1歩

SpringSecurity

認証機能が存在するWEBアプリケーションを開発する場合、認証に関連する機能をゼロから開発するのは大変ですね。
ちょっと考えただけで、以下のような制御が必要になります。


  • ログイン認証
  • セッション管理
  • 不正アクセスに対する制御
  • ワンタイムトークン

これらの、いわゆる認証で必要な機能を提供しているのが「SpringSecurity」です。
簡単な実装と設定ファイルへの設定記載をおこなえば、認証に必要な最低限の機能が使えるようになります。


今回は、認証機能が存在するWEBアプリケーションに対するSpringSecurityの使いかたを説明していきます。


ログイン認証

ログイン認証に関して、SpringSecurityでの実装はお決まりです。


applicationSecurity.xmlの基本的な定義


まずは、「applicationSecurity.xml」にSpringSecurityを使用するための定義が必要です。
以下に、定義例を記述します。


<sec:form-login login-page="/xxx.jsp"
    username-parameter="username"
    password-parameter="password"
    default-target-url="/xxx"
    always-use-default-target="true"
    login-processing-url="/j_spring_security_check"
    authentication-failure-handler-ref="authenticationFailureHandler"
    authentication-success-handler-ref="authenticationSuccessHandler" />

    <bean id="authenticationSuccessHandler" class="jp.co.xxx.AuthenticationSuccessHandler">
        <property name="alwaysUseDefaultTargetUrl" value="true" />
        <property name="defaultTargetUrl" value="/xxx/xxx" />
    </bean>

    <bean id="authenticationFailureHandler" class="jp.co.xxx.ExceptionMappingAuthenticationFailureHandler">
        <property name="defaultFailureUrl" value="/xxx" />
    </bean>

「xxx」の箇所は、SpringSecurityを組み込むWEBアプリケーション固有の部分になります。
<sec>タグで、SpringSecurityの基本動作を定義しています。
<sec>タグで、認証成功時と認証失敗時の制御クラスを定義しています。
「authenticationSuccessHandler」「authenticationFailureHandler」です。
その2つのクラスについて、<bean>タグでクラスの属性等を定義しています。


認証でのJava側の実装


Java側の実装においてポイントとなるのは、UserDetailsServiceクラスです。

UserDetailsServiceクラスのJavaDocは以下です。



で、UserDetailServiceを使った認証ロジックは以下になります。


import javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String loginID) {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) attrs.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        String password = request.getParameter("password");
    }
}

「loadUserByUsername」のパラメータである「loginID」が、ログイン画面で入力されたログインID。
HTTPリクエストから取得している「password」が、ログイン画面で入力されたパスワードです。


この実装をおこなうこで、「loadUserByUsername」でログインIDとパスワードがわかります。
後は、ログインIDとパスワードが格納されているテーブルを検索して、ログインIDとパスワードの一致判定をやればOKです。


ログインIDとパスワードが一致しているユーザ情報の取得でさえも、SprinSecurityが機能を提供をしています。
以下のような実装をおこなえば、DBに登録されているログインID・パスワードと一致するユーザ情報を取得できます。


import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

public class MyUserDetails extends User {
    public MyUserDetails(
        String userName,
        String password,
        String dispName,
        boolean enabled,
        boolean accountNonExpired,
        boolean credentialsNonExpired,
        boolean accountNonLocked,
        Collection<GrantedAuthority> authorities) {
        super(userName, password, enabled, accountNonExpired,
            credentialsNonExpired, accountNonLocked, authorities);
    }
}

※SpringSecurityは、ユーザロール(ユーザ毎の権限制御)についても制御可能ですが、今回のサンプルでは省略します。


上記クラスを作成し、「loadUserByUsername」において「MyUserDetails」のインスタンスを取得します。
以下のような感じです。


user = new MyUserDetails(
     ※ログインID,
     ※パスワード,
     ※ユーザ名,
     true,
     true,
     true,
     true,
     authorities);

インスタンスが取得できたら認証成功、インスタンスが取得できなかったら認証失敗、です


システムによっては認証が成功したとしても、ログイン成功とはみなさないケースもあるかと思います。
例えば、アカウントロック。
認証失敗した数をDBに保存しておいて、その数が規定値を超えたら画面にエラーメッセージを表示してログインさせない。


そういったシステム固有の認証チェックについても、この「loadUserByUsername」に実装するとよいでしょう。


認証に成功したら、ログイン情報をセッションに格納します。


    UserSession userSession = new UserSession();
    userSession.setLoginID(※ログインID);
    userSession.setUserName(※ユーザ名);
    RequestContextHolder.getRequestAttributes().setAttribute("session", userSession, RequestAttributes.SCOPE_SESSION);

userSessionというのは、システム固有でセッションとして保持しておきたい情報を格納するインスタンス。
そのインスタンスをセッションとして格納しています。


上記のサンプルプログラムは、ログインIDとユーザ名をセッションに格納する例です。
ここら辺はシステムによって変わってくるかと思います。
セッションとして保持するべきユーザ情報を見極めて、セッションに格納するようにしましょう。


認証に成功してセッションに情報を格納したら、認証については完了です。


ここまで記述した認証をおこなうためには、前述したSpringSecurityの定義の他に、「applicationSecurity.xml」に以下の定義が必要です。


<sec:authentication-manager>
    <sec:authentication-provider user-service-ref="myUserDetailsService" >
        <sec:password-encoder ref="passwordEncoder" />
    </sec:authentication-provider>
    </sec:authentication-manager>
    <sec:global-method-security pre-post-annotations="enabled"/>
    <bean id="myUserDetailsService" class="jp.co.xxx.MyUserDetailsService" />
    <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />

「jp.co.xxx.MyUserDetailsService」の「xxx」部分は、このクラスがシステム固有のクラスになるために「xxx」にしています。
システム固有のパッケージになります。


「jp.co.xxx.AuthenticationSuccessHandler」「xxx」部分は、このクラスがシステム固有のクラスになるために「xxx」にしています。
システム固有のパッケージになります。
この「AuthenticationSuccessHandler」に、ログイン成功時の制御を記述します。
固有のログを出すとか、ログイン成功回数をカウントするとか、業務に合わせた制御について、このクラスに処理を記述すればよいかと思います。


「value=”/xxx/xxx”」が、ログイン成功時に遷移するURLです。
デフォルトのURLになりますので、例外的に他URLに遷移させたい場合は、「AuthenticationSuccessHandler」で他URLに遷移する制御をおこなえばよいです。


パスワードの暗号化


パスワードはDBに登録することになりますが、一般的に平文(そのままの文字列)で登録することはセキュリティの観点からNGです。
何かしら暗号化して登録することが一般的です。


SpringSecurityは、暗号化されたパスワード文字列を参照しての認証についても自動で行ってくれます。
暗号化の種類は選べるのですが、以下の例は「SHA-1」で暗号化(ハッシュ化)された文字列をパスワードとして扱う場合の例です。
以下の定義を「applicationSecurity.xml」に定義すればよいです。


<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" >
    <constructor-arg value="1"/>
</bean>

「constructor-arg」がSHA強度です。
「1, 256, 384, 512」といった値を設定するのが一般的。


「applicationSecurity.xml」に定義するのは認証時の制御について必要になるのですが、パスワード文字列を登録する処理は別途開発が必要です。
多分、ほとんどのシステムがユーザ登録画面があり、そこからパスワード文字列を登録することになるかと思うので、その制御にパスワード文字列を暗号化してDBに登録する制御が必要になります。


SpringSecurityとは別に開発が必要ですが、難しい処理ではないです。


public static String getHashString(String str) {

    // ダイジェスト文字列取得
    MessageDigest md = null;
    try {
        md = MessageDigest.getInstance("SHA1");
    } catch (NoSuchAlgorithmException e) {
        return "";
    }

    // ダイジェスト文字列をハッシュ文字列に変換する
    byte[] dat = str.getBytes();
    md.update(dat);
    byte[] byt = md.digest();
    StringBuffer buff = new StringBuffer(byt.length * 2);
    for (int lc = 0; lc < byt.length; lc++) {
        int d = byt[lc] & 0xFF;
        if (d < 16) {
            buff.append("0");
        }
        buff.append(Integer.toHexString(d));
    }
    return buff.toString();
    }

実際のシステムでは、ユーザ情報の登録編集時に上記のようなロジックを組み込み、DBに暗号化されたパスワード文字列を登録します。


不正アクセスに対する制御(フィルター)


SpringSecurityを使って認証をおこなった場合、ログイン後の画面は認証に成功した状態じゃないとアクセスできません。
しかし、SpringSecurityの対象外にしたい画面があったります。
例えば以下のような画面。


  • ログインしてなくても使える画面(パスワード再発行画面、など)
  • CSSやJSファイル

上記のようにSpringSecutiryの対象外にしたい画面やファイルについては、「applicationSecurity.xml」に定義をおこなうことで設定できます。
フィルター定義ですね。


    <!-- SpringSecurity Filter対象外URL設定 -->
    <sec:http pattern="/css/**" security="none"/>
    <sec:http pattern="/js/**" security="none"/>

上記定義例は、ルートフォルダにある「css」フォルダと「js」フォルダ直下の全てのファイルについて、SpringSecurityの対象外にしています。
この定義をおこなわないと、画面にCSSが反映されず、かつ、JavaScriptも動かないので必須の定義になります。


CSRFトークン


SpringSecurityは、CSRFチェックをおこなうトークン値を自動で発行してくれます。
CSRFはクロスサイトリクエストフォージェリ”のことで、サイバー攻撃の一種です。


CSRFの詳しい説明は、以下のサイトにきれいまとまっています。



要はリクエストの改ざんです。
悪意のある第三者が、リクエストを偽造してWEBサイトにアクセスするサイバー攻撃になります。


これを防ぐ手段の一つがCSRFトークンです。
HTTPリクエスト内でCSRFトークンというシステムが発行した文字列を受け渡します。

このトークン値がシステムが期待している値と違ったら不正なアクセスと判断し、SpringSecurityがアクセスを遮断します。


ちょっと小難しいですが、こういった制御をSpringSecurityは自動でやってくれます。
JSP内にCSRFトークンを埋め込む処理を実装するだけです。


具体的には、以下の記述をおこないます。
これはJSPの場合、ですが。


<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

基本はログイン画面でのみ埋め込みばよい。
SpringSecurityは、どうやら1回発行したトークン値を使い回すようです。


業務システムだとブラウザの戻るボタンを許可しない場合もありますが、その場合は上記のコードを全画面のJSPの埋め込む必要があります。
全ての画面に埋めこむと、戻るボタンで戻った先の画面のCSRFトークン値が古くなってしまい、トークン値不一致でSpringSecurityが不正アクセスと判断します


仮に不正アクセスが発生した場合の制御については、「applicationSecurity.xml」に定義したクラスで制御可能。


<bean id="requestDataValueProcessor"
    class="jp.co.xxx.CompositeRequestDataValueProcessor">
    <property name="processors">
        <list>
            <bean class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" />
        </list>
    </property>
</bean>

まとめ


いかがでしたでしょうか?


世の中に存在するほぼすべてのWEBシステムが認証機能をもっているかと思いますが、認証機能をゼロから作るのは骨が折れます。。。
SpringSecurityを導入すれば、認証に必要な機能をゼロから作る必要はありません。


XMLの定義で結構必要なところが解り辛い部分なのですが、使わない手はないかと。


それではまた!



Apache Solrを使ったドキュメント検索 – FastVector Highlighter

ApacheSolr

前回、「Standard Highlighter」を使ったドキュメント検索、および、ハイライトを紹介しました。



今回は、ハイライト表示をおこなうことができる別の方法である「FastVector Highlighter」の使用方法を紹介します。


環境情報


環境情報は以下になります。

使用するのは、JavaとApache Solrの2つのみです。

  • Java 12.0.1
  • ApacheSolr 6.4.2

設定変更


「Standard Highlighter」でのハイライト表示をおこなった後に、「FastVector Highlighter」を使えるように設定変更をおこないます。
「Standard Highlighter」のスキーマ定義については、前回の記事を参考にしてください。



「Standard Highlighter」を使っている状態から、「FastVector Highlighter」を使える状態に変更します。


まずはスキーマ定義です。
今回は、文書の中身に対して検索をおこない、検索結果をハイライト表示することを想定します。
文書の中身を格納するフィールドである「content」について、以下のように設定変更をおこないます。
変更するファイルは「managed-schema」です。


<field name="content" type="text_general_content" multiValued="false" indexed="true" stored="true"/>

以下のように変更します。 ↓↓↓

<field name="content" type="text_general_content" multiValued="false" indexed="true" stored="true" termVectors="true" termPositions="true" termOffsets="true"/>

3つの設定値について、「true」を設定しています。


フィールド名

設定値

説明

termVectors

true=設定する false=設定しない

検索キーワードが含まれる数やキーワードの開始、終了位置などを結果に含めるかどうか(termPositionsとtermOffsetsをtrueに設定した場合、termVectorsもtrue扱いとなる)

termPositions

true=設定する false=設定しない

検索キーワードの含まれる位置を返すかどうか

termOffsets

true=設定する false=設定しない

検索キーワードの含まれる位置を返すかどうか


変更したら、Apache Solrのサービスを再起動して、変更した内容がコア定義に反映されているかを確認します。
Apache Solrのダッシュボードをひらき、コアの選択で対象コアを選択します。


コアを選択したら、左ペーンで「schema」を選択し、「content」フィールドを確認します。


「TermVector Stored」「Store Offset With TermVector」「Store Position With TermVector」が表示されチェックされていれば、スキーマの設定変更OKです。


ApacheSolrの設定確認

筆者は、この定義をおこなうことで「FastVector Highlighter」でのハイライト表示について思ったような結果が返却されるようになりました。


今回は、「Standard Highlighter」を使ったハイライト表示とまったく同じ結果にすることを目標にします。



その場合、ハイライト表示する際に検索ワードを囲むHTMLタグを固定させてしまおうと思います。
その場合、以下の定義変更が必要になります。
変更するファイルは「solrconfig.xml」です。


<fragmentsBuilder name="colored" class="solr.highlight.ScoreOrderFragmentsBuilder">
  <lst name="defaults">
    <str name="hl.tag.pre"><![CDATA[
      <b style="background:yellow">,<b style="background:lawgreen">,
      <b style="background:aquamarine">,<b style="background:magenta">,
      <b style="background:palegreen">,<b style="background:coral">,
      <b style="background:wheat">,<b style="background:khaki">,
      <b style="background:lime">,<b style="background:deepskyblue">]]></str>
    <str name="hl.tag.post"><![CDATA[</b>]]></str>
  </lst>
</fragmentsBuilder>

↓↓↓

<fragmentsBuilder name="colored" class="solr.highlight.ScoreOrderFragmentsBuilder">
  <lst name="defaults">
    <str name="hl.tag.pre"><![CDATA[<b>]]></str>
    <str name="hl.tag.post"><![CDATA[</b>]]></str>
  </lst>
</fragmentsBuilder>

「hl.tag.pre」について、<b>タグのみを使用するように変更します。


こうすることにより、検索ワードをbタグで囲みます。
こんな感じです。


  • <b>検索ワード</b>

FastVector Highlighterのサンプルコード


「FastVector Highlighter」を使った検索のサンプルコードは以下になります。


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.Group;
import org.apache.solr.client.solrj.response.GroupCommand;
import org.apache.solr.client.solrj.response.GroupResponse;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrDocument;

public class SolrSearchFast {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // Solrのインスタンス作成
        SolrClient client = new HttpSolrClient.Builder(
            "http://localhost:8983/solr/java_sample").build();

        SolrQuery solrQuery = new SolrQuery();

        // 検索結果として、文書IDを返却するよう設定
        solrQuery.setFields("id");

        // 検索結果の上限は100件
        solrQuery.setRows(100);

        try {
            StringBuilder queryString = new StringBuilder();
            String keyword = args[0];
            if (keyword.equals("")) {
                queryString.append("*");
            } else {
                String queryPhrase = "\"" + ClientUtils.escapeQueryChars(keyword) + "\"";
                queryString.append("(");
                queryString.append("content:");
                queryString.append(queryPhrase);
                queryString.append(")");
            }

            // 検索実行
            System.out.println("q=" + queryString.toString());

            solrQuery.set("hl.useFastVectorHighlighter", "true");
            solrQuery.set("hl.fragmentsBuilder", "colored");
            solrQuery.set("hl", true);
            solrQuery.set("hl.fl", "content");
            solrQuery.set("hl.fragsize", 100);

            QueryResponse response = client.query(solrQuery);

            // 検索結果を表示
            SolrDocumentList list = response.getResults();
            if (list == null) {
                System.out.println("文書は存在しませんでした。");
            } else {
                System.out.println(list.getNumFound() + "件ヒットしました。");

                // ハイライト情報を取得して加工
                Map<String,Map<String,List<String>>> highlighting = response.getHighlighting();
                for (SolrDocument doc : list) {
                    System.out.println(doc.get("id"));
                    String id = (String) doc.getFieldValue("id");
                    Map<String, List<String>> map = highlighting.get(id);
                    List<String> contentList = map.get("content");
                    for(String val : contentList) {
                        val = val.replaceAll("[\r\n\t]", "");
                        System.out.println("val=" + val);
                    }
                }
            }
        } catch (SolrServerException e) {
            System.out.print("SolrServerException Occured!\r\n");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.print("IOException Occured!\r\n");
            e.printStackTrace();
        } finally {
            try {
                // コミットして、コネクションをクローズ
                client.commit();
                client.close();
            } catch (Exception e) {
                System.out.print("Exception Occured!\r\n");
                e.printStackTrace();
            }
        }
        System.out.print("end: main\r\n");
    }
}

「Standard Highlighter」を使ったハイライト表示をおこなうサンプルコードと、ほぼほぼ同様です。
違う箇所は、クエリパラメータだけ。



検索結果のハイライト文字列についても、「Standard Highlighter」を使用したサンプルコード検索結果と、ほぼ同様となります。


「Standard Highlighter」と「FastVector Highlighter」、どっちを使うべきか?


どちらを使っても同じハイライト表示をおこなうことができるので、機能としては同等です。

では、どちらを使った方がいいのか?ということですが、筆者は「FastVector Highlighter」を推します。
理由はパフォーマンスです。


筆者が検証した限りでは、「Standard Highlighter」と「FastVector Highlighter」では、約5倍の性能差がありそうです。

以下は、500ファイルが登録されたコアに対して、30ファイルがヒットする状態に対してのパフォーマンス比較です。


ApacheSolrの検索比較

縦軸はmsecです。
なので、「Standard Highlighter」が1秒ちょっとかかる検索に対して、「FastVector Highlighter」は0.2秒くらい。
パフォーマンスが圧倒的に違います。
こうなってくると、「FastVector Highlighter」を使った方がよいという話になります。


では、「FastVector Highlighter」を使うデメリットはまったくないかというとそうではないです。


「設定変更」で説明したスキーマ定義をおこなうと、ファイルを登録した際にコアに登録されている内容が変わってきます。
コアのファイルサイズが、変更前のスキーマ定義と比べると1.5倍になりました。


あまり深く調べられてまとめいないのですが、「termVectors」「termPositions」「termOffsets」をオンにすると、「FastVector Highlighter」を使うためのインデックス情報をApacheSolrが大量に作るようです。
そのために、ファイルサイズが膨大になるが、高速な検索が可能になるということなります。


まとめ


いかがでしたでしょうか?


「FastVector Highlighter」について説明してきましたが、前記事の「Standard Highlighter」と比べると違いがわかるかと。


まとめ
  • 「Standard Highlighter」ではなく「FastVectorHighlighter」を使った方が高速
  • でも、「Standard Highlighter」より「FastVectorHighlighter」の方がディスク使用率が高い

結論としては筆者は「FastVector Highlighter」推しです。
やはり、検索パフォーマンスが高いのは魅力的なので。


それではまた!



ファイルが削除されない原因は何?java.io.Fileとjava.nio.file.Filesの違いを検証

Java

この前、実際の業務でちょっと困ったことがおきました。
ちょっと前に作ったシステムなのですが、“ある特定のファイルが削除されない”という現象です。


今回は、実際におきた現象をもとに、「java.io.File」と「java.nio.file.Files」の違いを実際のプログラムを使って紹介していきます。


実際に起きた現象自体は解消されたのですが、一番困ったのは調査です。
原因は、削除するファイルが他プロセスにつかまれていることが原因だったのですが、その原因がすぐにはわからなかったです。


「java.io.File」を使ってファイル削除していたのですが、他プロセスが削除対象ファイルをつかんでいる場合は、返り値で失敗(false)を返却しているのみで、例外が発生しないです。
なので、削除されない原因がすぐにはわかりませんでした。


そのため、「java.io.File」ではなく「java.nio.file.Files」に切り替えて例外をログ出力するようにすることで、ようやく原因がわかりました。


サンプルプログラム

二つのプログラムで試してみます。
「java.io.File」で削除するプログラムは以下。

import java.io.IOException;
import java.io.File;

public class FileDelete {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        boolean isDone = false;
        try {
            File file = new File("C:\\Temp\\file.txt");
            isDone = file.delete();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.print("isDone=" + isDone + "\r\n");
        }
        System.out.print("end: main\r\n");
    }
}

「java.nio.file.Files」で削除するプログラムは以下。

import java.io.IOException;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class FileDeleteIO {

    public static void main(String[] args){
        System.out.print("start: main\r\n");
        try {
            Files.delete((Paths.get("C:\\Temp\\file.txt")));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.print("finally\r\n");
        }
        System.out.print("end: main\r\n");
    }
}

「java.io.File」は削除結果を返却してくれるので削除結果を標準出力していますが、「java.nio.file.Files」の方は削除結果を返却しないので何も標準出力していません。


この2つのプログラムを使用して、「java.io.File」と「java.nio.file.Files」の違いを検証します。
ファイルの削除について挙動の違いを検証します
以下の3パターンです。


  • 削除対象ファイルが存在しない場合
  • 削除対象ファイルに対して削除権限がない場合
  • 削除対象ファイルが他プロセスに開かれている場合

存在しないファイルを削除

削除対象のファイルが存在しない場合の挙動を試してみます。
「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.NoSuchFileException: C:\Temp\file.txt
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「NoSuchFileException」というファイルが存在しません、という例外です。
この例外を嫌う場合は「delete」ではなく「deleteIfExists」を使えばいいです。


削除権限がないファイルを削除

削除対象のファイルについて削除権限が存在しない場合の挙動を試してみます。
「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.AccessDeniedException: C:\Temp\file.txt
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:89)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「AccessDeniedException」という、ファイルへのアクセス権限が存在しません、という例外です。


他のプロセスが開いているファイルを削除

削除対象のファイルについて、他のプロセスがファイルを開いている場合の挙動について試してみます。


あえて他のプロセスがファイルをつかんでいる状態を発生させるために、簡単なプログラムを作りました。

import java.io.IOException;
import java.io.FileOutputStream;
import java.io.File;

import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLocker {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // 削除対象ファイルを読み込み
        File file = new File("C:\\Temp\\file.txt");
        FileChannel fc = null;
        FileLock lock = null;

        try {
            // ファイルを読み込み、ファイルをロック
            FileOutputStream fos = new FileOutputStream(file);
            fc = fos.getChannel();
            lock = fc.tryLock();

            // ロックに失敗した場合、例外をスロー
            if (lock == null) {
                throw new Exception("lock failer");
            }

            // 1秒スリープ
            System.out.print("sleeping...\r\n");
            Thread.sleep(60000); 
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (lock != null) {
                try {
                    lock.release();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        System.out.print("end: main\r\n");
    }
}

このプログラムでファイルをつかんでいる状態を作り出して、ファイル削除をためしてみます。


「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.FileSystemException: C:\Temp\file.txt: プロセスはファイルにアクセスできません。別のプロセスが使用中です。
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「FileSystemException」という例外で、ファイルシステムとしてはこのファイルが他プロセスにロックされてるという例外です。


筆者が直面した問題がまさにこれでして、「java.io.File」だと「false」とだけ出力されるので、何か原因で削除されなかったのかがわからない。。。
「java.nio.file.Files」だと例外が出力されるので、原因についてある程度のあたりがつきます。


まとめ

「java.io.File」と「java.nio.file.Files」、どっちを使う方がいいのでしょうか?
実装するアプリケーションの特性にもよるので一概にはどちらがいい、とは言えないのですが、筆者は今後は極力「java.nio.file.Files」を使うようにしようと思っています。


理由はやはり、なにかしらエラー(削除失敗、など)が発生した場合の原因調査。
例外がトレースとして出力されるのとされないのでは、障害発生時の調査に掛かる時間が圧倒的に違います。


今後は「java.nio.file.Files」を使うようにしよう。。。


それではまた!