Java9でJAXB

Java9でJAXB

単純なサーバ間のデータ連携アプリをjava9で動かすことにした。
すると

1
2
3
4
5
6
7
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/JAXB
        at Jaxb.main(Jaxb.java:72)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXB
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)
        ... 1 more

JAXBのライブラリは、Java9からmoduleとして定義され、自動で読み込まれなくなった。
Java9からはモジュールとして、自分で読み込む必要がある。
モジュールの読み込みは、javaコマンドの引数の–add-modulesで指定する。

1
$ java --add-modules java.xml.bind MyProgram

javax.xml.binはjava.xml.binモジュールに定義されているらしい。

ちなみに、どんなモジュールを利用しているかどうかは、Java9付属のjdepsというコマンドで確認できる。
MyProgram.classを確認してみた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ jdeps -cp . -verbose MyProgram.class
MyProgram.class -> .
MyProgram.class -> java.base
MyProgram.class -> java.xml.bind
    MyProgram  -> Col                         .
    MyProgram  -> Data                        .
    MyProgram  -> Row                         .
    MyProgram  -> java.io.File                java.base
    MyProgram  -> java.io.PrintStream         java.base
    MyProgram  -> java.lang.Class             java.base
    MyProgram  -> java.lang.Exception         java.base
    MyProgram  -> java.lang.Integer           java.base
    MyProgram  -> java.lang.Object            java.base
    MyProgram  -> java.lang.RuntimeException  java.base
    MyProgram  -> java.lang.String            java.base
    MyProgram  -> java.lang.StringBuilder     java.base
    MyProgram  -> java.lang.System            java.base
    MyProgram  -> java.lang.Throwable         java.base
    MyProgram  -> java.util.Iterator          java.base
    MyProgram  -> java.util.List              java.base
    MyProgram  -> javax.xml.bind.JAXB         java.xml.bind

最初の方に、利用しているモジュールの一覧が表示された。
その次に、利用しているクラスの一覧が表示された。
Java9のjdepsからは、利用しているクラスの右に、それらが格納されるモジュールが表示される。

これで、利用しているモジュールがわかるので、実行時に指定するようにする。
Project Jigsawは、他人事かと思っていたが、関係してきているようだ。

同じタグの記事
同じカテゴリの記事

javaでダミー画像作成

javaでダミー画像作成

商用環境のデータをテスト環境へコピーした。
データベースの個人情報にはマスクをした。
何が保存されているかわからない、画像のコンテンツはすべて削除した。

しかし画像コンテンツが何もないと、テストでの表示がうまくいっているのかわからない。
それぞれのデータ用に、ファイル名が書かれたPNGイメージを一括作成した。

java 8 の jjs (Nashorn) で作成した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var File = Java.type('java.io.File');
var Color = Java.type('java.awt.Color');
var Font = Java.type('java.awt.Font');
var BufferedImage = Java.type('java.awt.image.BufferedImage');
var RenderingHints = Java.type('java.awt.RenderingHints');
var ImageIO = Java.type('javax.imageio.ImageIO');

function createImage(filename) {
    var bufferedImage = new BufferedImage(200, 60, BufferedImage.TYPE_INT_RGB);
    var graphics2D = bufferedImage.createGraphics();
    //
    graphics2D.setColor(Color.BLUE);
    graphics2D.fillRect(0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
    //
    var font = new Font('Consolas', Font.PLAIN, 30);
    graphics2D.setFont(font);
    graphics2D.setColor(Color.WHITE);
    graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    graphics2D.drawString(filename, 5, 40);
    // output
    ImageIO.write(bufferedImage, "png", new File(filename));
}

createImage('test.png');

概要

1.BufferedImageを作り、そこからGraphics2Dを取り出す。
2.Graphics2Dに対して描画する。
3.ImageIOを利用して、ファイルに書き出す。

これだけ。

簡単にpngを作成できた。
java8だと、ほかにJPGやGIF、BMPが利用できる。
java9だと、TIFFなども利用できる。
しかし、BMPやTIFFはファイルサイズが大きくなるので利用するときはディスク容量に気を付ける必要がある。

また、文字を出力する場合、何も指定しないとアンチエイリアスされず、ギザギザした文字が描画されてしまう。
そこで、RenderingHintを設定して、アンチエイリアスした文字を書き込むようにする。
これで、綺麗な文字が出力される。

同じタグの記事
同じカテゴリの記事

javaでイメージフォーマット変換

javaでイメージフォーマット変換

書類を管理しているシステムがある。
書類の解像度を落としたくないため、TIFFフォーマットで保存されている。
しかし、TIFFフォーマットはブラウザで直接ビューイングできないので、ウェブアプリでは利用しにくい。
javaでTIFFからJPGに変換してみる。

javaでテストするのは面倒なので、jjs (java9 Nashorn)で作ってみる。

1
2
3
4
5
6
7
8
9
var ImageIO = Java.type('javax.imageio.ImageIO');
var File = Java.type('java.io.File');

function convert(filename, toFormat) {
    var imageIo = ImageIO.read(new File(filename));
    ImageIO.write(imageIo, toFormat, new File(filename + '.' + toFormat))
}

convert('test.tif', 'png');

簡単にpngに変換できてしまう。
ただし、TIFFを扱うためには java 9 が必要。
java 8でもpng,jpg,gif,bmp間の変換は可能。

コマンドラインツールを使ってもいいけど、サーバによってはプログラムのインストールが禁止されているので、
簡単に実行環境が作成できるjavaで実装した。

同じタグの記事
同じカテゴリの記事

Java DateFormat

Java DateFormat

多国籍企業で全世界で動くシステムがある。
日本主導のシステムだが、日付フォーマットを各国のフォーマットに対応させている。
日本ではyyyy/MM/dd、アメリカではMMM d, yyyy、フランスではd MMM yyyy。
Webシステムで、日付はカレンダーから選択する。
OracleにインサートするときはCHAR(10)に対してYYYY/MM/DDで登録する。

こういう感じでYYYY/MM/DD形式の日付に変換してみた。

1
2
3
4
5
6
7
java.lang.String requestDate = request.getParameter("requestDate");
java.util.Locale locale = request.getLocale();

java.text.SimpleDateFormat formatYmd = new java.text.SimpleDateFormat("yyyy/MM/dd");
java.text.DateFormat formatLocal = java.text.DateFormat.getDateInstance(2, locale);

String requestDateYmd = formatYmd.format(formatLocal.parse(requestDate));

OracleのカラムをDATEで定義すると、今度はタイムゾーンの問題が発生する。
これはまた後日。

同じタグの記事
同じカテゴリの記事

Oracleのjdbc connectionが

Oracleのjdbc.connectionが

1週間で、50回程度呼び出されたJavaバッチで
「IOエラー: Connection reset」が2度発生した。
最初は、不調なOracleと脆弱なネットワーク環境を疑ったが、
他のバッチでエラーが発生していないので、ネットで検索してみた。

Linuxの乱数のとり方に問題があるためらしい。
接続に時間がかかることもあるようだ。

Oracleのjdbcドライバが/dev/randomを利用していることが原因との事。
乱数の取り方を変える方法はjavaオプションを指定する。

1
java -Djava.security.egd=/dev/urandom program.Program

これで、java.sql.SQLRecoverableException: IOエラー: Connection resetを抑制できる(はず)。

同じタグの記事
同じカテゴリの記事

EclipseLinkでJPA

EclipseLinkでJPA

EclipseLinkを利用し、JPAでデータベース更新をしようと考えた。
サンプルを作成し、動かしてみたが下記のような課題が検出された。

実装について

  • データ登録(INSERT)

事前に作成しておいたEntityクラスのインスタンスを作成し、そのプロパティをセット→EntityManager.persist(entity)で
レコードの登録ができる。お手軽。
EntityTransaction.commit()を実行しないとデータ更新はされない。

  • データ更新(UPDATE)

更新対象レコードのEntityをEntitiManager.find(Entity.class, pk)で取得し、取得できたEntityのプロパティを更新するだけで、
レコードの更新ができる。お手軽。
更新はEntityManager.persistする必要もない。
EntityTransaction.commit()を実行すればよい。
登録のアクションが必要ないのは不安に思われることもあるかもしれないが、
エラー時はEntityManager.rollback()すれば更新されないので、更新忘れがなくて良い仕様だと思う。

  • データ削除(DELETE)

これもとても簡単。EntityManager.find()で取得したentityをEntityManager.remove(entity)するだけ。

  • データ取得(SELECT)

キーで取得するのは簡単。EntityManager.find(Entity.class, pk)で取得できる。簡単。
リストで取るときは、多少癖がある。
また、いくつか異なるアクセス方法がある。

1.SQLを使う方法
2.JPQLを使う方法
3.CriteriaQueryを使う方法

1.はコンパイラ警告が出るので使わないほうが良いかと思う。(SuppressWarningsして使ってもいいけど)
2.の方法がとっつきやすい気がする。
3.CriteriaQuery、Root、などなど、よくわからないクラスを使う。サンプルを見ながらでも、どのクラスがどの役割なのかわかりにくい。

  • Entity作成にあたって

Entityクラスを生成するのは結構面倒。
特に複合主キーの場合。キーのみをクラス(内部クラスでも可)として定義する必要がある。
JPAでは主キーを単一項目にすることが推奨されているらしい。
すでに複合キーのテーブルで検証したため、なかなか面倒だった。

1
2
3
4
5
6
7
CREATE TABLE T01 (
    C01 CHAR(10) NOT NULL,
    C02 CHAR(2) NOT NULL,
    C03 VARCHAR2(100),
    C04 VARCHAR2(100),
    PRIMARY KEY (C01, C02)
)

上記のテーブルの場合、Entityは下記のようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package entity;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;

@Entity(name = "T01")
public class T01 {

    @EmbeddedId
    public PK pk;

    @Embeddable
    public static class PK {
        @Column(name = "C01", nullable = false)
        public String c01;

        @Column(name = "C02", nullable = false)
        public String c02;

        public boolean equals(Object obj) {
            PK tgt = (PK) obj;
            return c01.equals(tgt.c01) && c02.equals(tgt.c02);
        }

        public int hashCode() {
            return (c01 + c02).hashCode();
        }
    }

    @Column(name = "C03", nullable = false)
    public String c03;

    @Column(name = "C04", nullable = false)
    public String c04;
}

実行環境について

  • EclipseLinkのバージョン

最新バージョンは2.7.0。これはjava8以降で動作する。
あえてjava7で利用したい場合は、2.6.4を利用する。

  • Entityをjarファイルにまとめている場合

Entityクラスがjarファイルに入っている場合は、persistence.xmlに設定が必要。
classファイルのまま、クラスパスにおいてある場合は、@Entityのクラスを自動的に読んでくれる。

jarファイルに入っている場合は、

1.Entityクラスをとして宣言しておく。
これはすべてのクラスをここに書かなくてはならないので、追加・変更時のメンテンナンスがなかなか大変。

2.Entityクラスが入っているjarファイルをに宣言しておく。
これであれば、jarファイル内の@EntityのクラスをEntityとして自動的に読んでくれる。

ただし、jarファイルを参照するパスが独特なので注意する必要がある。
絶対パスで指定する場合は、あまり気にしなくてよいが、通常はディレクトリ構成に左右されないように相対パスで指定すると思う。

カレントディレクトリはクラスパスで最初に見つかったMETA-INF/persistence.xmlが存在するディレクトリとなる。
たとえば、top_dir/conf_dir/META-INF/persistence.xmlの様にpersistence.xmlが存在する場合、top_dir/conf_dirがカレントディレクトリとなる。
したがって、top_dir/libにエンティティが収められたjarが保存されていた場合、persistence.xmlには下記のように書く。
../lib/my_entities.jar
ここに宣言されたjarファイルはクラスパスを追って読むわけではないので注意が必要。

同じタグの記事
同じカテゴリの記事

jconsoleでリモート接続するために

jconsoleでリモート接続するために

java作成された常駐アプリが、元気に動いているかどうか確認したい場合がある。
jconsoleで接続すれば、グラフィカルにCUPやメモリの状況を確認できる。
サーバで動くアプリの場合、サーバのGUIに接続できれば、特に準備などなしにjconsoleで接続できるが、
サーバのGUIに接続できない場合、クライアントのjconsoleから接続する必要がある。
そうしたいときはjavaの常駐アプリを起動する時に引数を与えておく必要がある。

1
java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=12345 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false test.JavaProgram

このように、test.JavaProgramを起動すると、ローカルのjconsoleからport:12345で接続できるようになる。

また、常駐アプリがtomcatの場合、setenv.shに下記の設定を追加すれば良い。

1
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=12345 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"

なお、setenv.shは$CATALINE_HOME/binに作成すれば良い。

また、$CATALINE_HOMEはtomcatを解凍したディレクトリの事(中にbinやconf,webappsが入っているディレクトリ)。

ヒープの利用状況、GC実行状況が見えたり、CPU使用率、スレッド数など、活動状況を確認できる。
また、MBeansに接続できるので、オリジナルのアプリであれば、外部インターフェースとしても利用できる。

サーバがLinuxでssh接続できるのであれば、ssh -X で接続して、サーバのXを転送する方法もある。
しかし、Xクライアントによっては、画面は見えるけどMBeanの引数を設定できなかったりする。

同じタグの記事
同じカテゴリの記事

itext5でPDFを暗号化する

itext5でPDFを暗号化する

PDFファイルを暗号化したかったわけではないが、
暗号化したPDFをフィルタリングする機能を作成したかった。
そこで、サンプルの暗号化されたPDFを探したが、256bitAESで暗号化されたファイルが見つからなかった。
しかたないので、適当なPDFを256bitAES暗号化するプログラムを作成した。

itext5を利用している。
暗号化が関連するため、bcprovが必要になる。

下記の2つのjarファイルをeclipseのbuild pathに追加した。

  • itext5-itextpdf-5.5.12.jar
  • bcprov-jdk15on-158.jar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package test.pdf;

import java.io.FileOutputStream;

import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfWriter;

public class Enc {
    public static void main(String[] args) {
        System.out.println("start.");
        PdfReader reader = null;
        PdfStamper stamper = null;
        try {
            //
            byte[] ownerPassword = args[0].getBytes();
            byte[] userPassword = args[1].getBytes();
            reader = new PdfReader(args[2]);
            //
            stamper = new PdfStamper(reader, new FileOutputStream(args[3]));
            stamper.setEncryption(userPassword, ownerPassword, PdfWriter.ALLOW_SCREENREADERS, PdfWriter.ENCRYPTION_AES_256);
            //
            System.out.println("end.");
        } catch (Exception e) {
            System.out.println("error. " + e.toString());
        } finally {
            try {
                stamper.close();
            } catch (Exception e) {
                System.out.println("stamper close failed. " + e.toString());
            }
            try {
                reader.close();
            } catch (Exception e) {
                System.out.println("reader close failed. " + e.toString());
            }
        }
    }
}

下記の通り実行すると、暗号化されていないPDFを暗号化した新しいファイルが保存される。

1
2
export CLASSPATH=itext5-itextpdf-5.5.12.jar:bcprov-jdk15on-158.jar:.:$CLASSPATH
java test.pdf.Enc 99999 "" orginal.pdf encrypted.pdf

99999は管理者パスワード
“”はユーザパスワードなしを示している
original.pdfを暗号化し、encrypted.pdfとして出力する。

同じタグの記事
同じカテゴリの記事

[java] StandardOpenOption.DELETE_ON_CLOSE

StandardOpenOption.DELETE_ON_CLOSE

java.nio.fileのStandardOpenOptionにはDELETE_ON_CLOSEという定数が定義されている。
この定数をファイルのオープン時のオプションとして利用すると、ファイルをクローズした時に自動的にファイルが削除される。
下記の通り利用する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class DeleteOnCloseTest {
    public static void main(String[] args) {
        Path file1 = Paths.get("C:", "file1.txt");
        try (BufferedWriter writer = Files.newBufferedWriter(file1, StandardCharsets.UTF_8,
             StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING,
             StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
            writer.write("test1-1\n");
            writer.write("test1-2\n");
            writer.write("test1-3\n");
            System.out.println("1.file1 exists? " + Files.exists(file1));
        } catch (Exception e) {
            System.out.println("create file failed. " + e.toString());
        }
        System.out.println("2.file1 exists? " + Files.exists(file1));
    }
}

実行すると、

1.file1 exists? true
2.file1 exists? false

と出力される。
中間ファイルとして利用するのに便利なのかもしれないが、
もし、出力中に想定外のExceptionが発生した場合は、原因追求のためにファイルは削除せずに残しておきたい。
したがって、こういう使い方はあまりしない気がする。

どちらかと言えば、読み込みオープンの時に指定できれば、中間ファイルを読んで、読み終わったら削除できるので便利な気がする。
しかし、このオペレーションはサポートされていない。
Files.newBufferedReaderではOpenOptionが指定できないし、
Files.newInputSreamでOpenOptionを指定しても下記の通り例外が発生する。

1
java.lang.UnsupportedOperationException: 'DELETE_ON_CLOSE' not allowed

ちなみに、この自動削除の仕組みを利用してプログラムを作成するのであれば、
中間ファイル名は自動でjavaに決めてもらった方が簡単な気がする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package test.java;

import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class DeleteOnCloseTest {
    public static void main(String[] args) {
        Path file3 = null;
        try {
            file3 = Files.createTempFile(null, null);
        } catch (Exception e) {
            System.out.println("create temp file failed. " + e.toString());
            throw new RuntimeException(e);
        }
        System.out.println("work file name:" + file3.toString());
        try (BufferedWriter writer = Files.newBufferedWriter(file3, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
            writer.write("test3-1\n");
            writer.write("test3-2\n");
            writer.write("test3-3\n");
            System.out.println("1.file3 exists? " + Files.exists(file3));
        } catch (Exception e) {
            System.out.println("write file failed. " + e.toString());
        }
        System.out.println("2.file3 exists? " + Files.exists(file3));
    }
}

実行すると、下記の通り出力される。

work file name:C:\Users\xxxxx\AppData\Local\Temp\6712414364776398896.tmp
1.file3 exists? true
2.file3 exists? false

Files.createTempFileで一時ファイルを作成できる。第一引数は中間ファイルのプレフィックス。第二引数は中間ファイルのサフィックス。
どうせ、見えずに消えてしまうファイルなので、どちらの引数もnullを指定している。
これで、ファイル名の重複を気にせず、中間ファイルを作成できる。

また、これらの処理がバッチ処理で、javaのプロセスが適宜終了するのであれば、下記の通りファイルに削除設定をしておいても、プロセス終了時に中間ファイルが削除される。

1
2
Path file = Paths.get("workfile");
file.toFile().deleteOnExit();
同じタグの記事
同じカテゴリの記事

オレオレ証明書サーバに接続

オレオレ証明書サーバに接続する

java(7~)でオレオレ証明書のサーバにssl接続すると、証明書エラーが発生する。
こういう場合は、そのオレオレ証明書をJavaのキーストアに登録して、そのキーストアでサーバを認証してアクセスする。なお、それように新しいキーストアを複数作成することができる。

キーストアを作成する

任意のcrtファイルを取得し、そのcrtファイルを含むキーストアを作成する。

1
keytool -importcert -file サーバ証明書.crt -keysore my.ks -alias 任意の証明書につける名前  -storepass changeit

-storepassを指定しなかった場合は、プロンプトで確認される。
なお、デフォルトのキーストアのパスワードはchangeitらしい。

作成したキーストアファイルを利用してjavaプロセスを起動する。

1
java -Djavax.net.ssl.trustStore=C:\my.ks -Djava.net.ssl.trustStorePassword=changeit my.java.Program

これで、任意の証明書を信頼済みとしてhttpsのサーバにアクセスできる。
なお、サーバ名と証明書のサーバ名が不一致の場合は、流石に補正できないので、そこはサーバ側で合わせておく。
クライアントのhostsで補正しても良い。

同じタグの記事
同じカテゴリの記事