IT社畜犬くわっちょのはてな

青森の片隅で働く、フルスタックエンジニアに憧れる器用貧乏なIT社畜犬の遠吠え

東京の会社を退職し青森にUターンした話

itdogkuwaccho.hatenadiary.com

渋谷のIT企業を退職して早いものでもう三ヵ月ほど過ぎました。 転職の理由などは上記にある通りなので青森に転職しての感想など。

10年近く前のコードが現役

これは青森に限った話ではないでしょうが、10年前以上のJavaソースが脈々と使われているのを見るとなんともなあという気持ちになります。

開発環境は最新。本番環境は長期稼働

開発用PCはWindows10など最新のものなのですが、本番機は結構キャリア長め。もちろんアップデートはされているのですが、社内用テスト環境がWindowsXPWindows2000とかはある

外注探すのも一苦労

プログラミングを外注しようにもSI企業はほとんどないので仕事がなくてもエンジニアをキープするためにお金を払う......とかがある。

求められるスキルは幅広いものになる

エンジニアを集めようにも東京のようにはいかないので、どうしても全般的に詳しい人が求められる。

仕事の自由度は高い

その分自分で決められる範囲は広い。責任は伴うけれど。

残業はほとんどない

定時で帰る人が結構多いです。

最後に

青森に帰ってきたこと自体は後悔していません。 しかし、時々東京での騒がしい日々は懐かしくなります。 今の自分を大切にしながら、青森で、今のところで、役に立てるように頑張っていきます。

JDK 1.8.0_152での無制限強度暗号ポリシーの変更方法 ※Java9も少し含む

今更だけどJCE policyの解除方法がかわったっぽいので検証。

検証プログラム https://github.com/kuwaccho/Verification/blob/master/src/AesCryptTest.java

  • Java8の場合

JDKJDK 8u152を使用

keyが128bitの場合

PLAIN TEXT: (・ω・`U)
ENCRYPTED : cOipgSYWi6/AXsfjTNmysQ==
DECRYPTED : (・ω・`U)

何もしないでkeyが256bitの場合

PLAIN TEXT: (・ω・`U)
java.security.InvalidKeyException: Illegal key size
    at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1039)
    at javax.crypto.Cipher.implInit(Cipher.java:805)
    at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
    at javax.crypto.Cipher.init(Cipher.java:1396)
    at javax.crypto.Cipher.init(Cipher.java:1327)
    at AesCryptTest.main(AesCryptTest.java:23)

jdk1.8.0_152/jre/lib/security/java.security の826行目にある以下のプロパティのコメントアウトを解除

#crypto.policy=unlimited
↓
crypto.policy=unlimited

結果

PLAIN TEXT: (・ω・`U)
ENCRYPTED : 3858uw2xCZPbmpV9xs4/eA==
DECRYPTED : (・ω・`U)

ちなみに

jdk1.8.0_152/jre/lib/security/policy
                ├─limited
                │      local_policy.jar
                │      US_export_policy.jar
                │
                └─unlimited
                        local_policy.jar
                        US_export_policy.jar

とあるのでこのあたりを切り分けているんだろうなあ。

あと Java™ SE Development Kit 8, Update 152 Release Notes にこんなこと書かれているので注意。

Because the old JCE jurisdiction files are left in <java-home>/lib/security, they may not meet the latest security JAR signing standards, which were refreshed in 6u131, 7u121, 8u111, and later updates. An exception similar to the following might be seen if the old files are used:

Caused by: java.lang.SecurityException: Jurisdiction policy files are not signed by trusted signers! at javax.crypto.JceSecurity.loadPolicies(JceSecurity.java:593) at javax.crypto.JceSecurity.setupJurisdictionPolicies(JceSecurity.java:524)

  • Java9の場合

Java9の場合はjava.securityの場所がconf以下に変更になっているのと、最初からコメントアウトはされていない(832行目付近参照)ので 恐らく何もしないで動くのだろう......。

JDK 9 Readme

The default JCE policy files bundled in this Java Runtime Environment allow for "unlimited" cryptographic strengths.
For convenience, this software also contains the historic "limited" strength policy files which restricts cryptographic strengths. To use the limited strength policy, instead of the default unlimited policy, you must update the "crypto.policy" Security property (in /conf/security/java.security) to point to the appropriate directory. You are advised to consult your export/import control counsel or attorney to determine the exact requirements of your location, and what policy settings should be used.

弁護士とちゃんと相談しろよという一文がなんかひっかかるが。

AESの暗号鍵列長のポリシーが変更になった理由は何なのかは気になる。 アメリカ政府の方針変換なのか、そもそも年数がたちすぎているからかセキュリティの問題か......。

メジャーなRDBMSでREPLACE関数が空文字を返すかnullを返すか調べてみた

青森に帰ってきてから仕事ではOracleを使うようになった。 今までは予算の都合から使える事はなかったのだが......。

そんな中、2017年にもなってOracleを使う人ならまず引っかからないであろう奇妙な挙動に引っかかる。

SELECT 'TEST'
FROM dual 
WHERE
  REPLACE (REPLACE (' テスト ', ' ', ''), ' ', '') = 'テスト' 
  AND REPLACE ('//', '//', '') = ''

上記SQL、実際に実行していただければわかると思うが 結果は何も出ないのだ。

原因は5行目、ここを空文字比較しているのが問題。 Oralceでは上記のようにREPLACEを行って空文字になるとnullと解釈する。

なので上記のSQL

SELECT 'TEST'
FROM dual 
WHERE
  REPLACE (REPLACE (' テスト ', ' ', ''), ' ', '') = 'テスト' 
  AND REPLACE ('//', '//', '') IS NULL

と IS NULL を使えばちゃんとtrueになって値が取得できるというわけだ。

......いやまて、なぜ0byte文字列とnullが同値扱いになる? ※実際には空文字比較でfalseなので空文字はnullへの不可逆変換なわけだが。

これに関しては

と教えていただいてそこにはさらに

By the time that the SQL standard came around and agreed that NULL and the empty string were distinct entities, there were already Oracle users that had code that assumed the two were equivalent. So Oracle was basically left with the options of breaking existing code, violating the SQL standard, or introducing some sort of initialization parameter that would change the functionality of potentially large number of queries. Violating the SQL standard (IMHO) was the least disruptive of these three options.

とあるので標準化が進む中で、既にもう既存コードが置き換えするのが難しいくらいになってきているのであえて規則に違反する道を選んだのではないかと言われている。 確かにこれはうなずける部分もある。上記のSQLの挙動が変われば影響あるシステムはいくつもあるはずだ。

しかしそれはあくまでnullとempty Stringの問題であってREPLACE関数まで同様にするのは正直どうかと思う。 REPLACEで置き換えてempty Stringになった時点でもうnullと比較しないといけないということじゃないか。

SELECT 'TEST'
FROM dual 
WHERE
  REPLACE (REPLACE (' テスト ', ' ', ''), ' ', '') = 'テスト' 
  AND REPLACE ('//', '//', ' ') = ' '

あるいはまあこう書くのも一つの手なのかもしれないが、なんだかこれもバッドノウハウだと思う。 空白で置き換えるというのがややこしい。

ついでなのでメジャーなRDBMSでREPLACE関数で空文字になった場合にどのような挙動をするか調べてみた。

一番最後のは趣味だが......。 結果は案の定だったけれど一応検証用クエリを載せる。 テーブルを作成する必要がないからDBをインストールした状態であればすぐに追従はできるはずだ。

SQL Server

SELECT NULLIF(REPLACE ('//', '//', ''), '') ⇒ null
SELECT NULLIF(REPLACE ('//', '//', ''), null) ⇒ 空文字

※REPLACE結果は空文字扱い

Oracle

上記のとおり空文字はnull扱い これを流せば「(・ω・`U)null」となるはずだ。

SELECT
  '//'
  , NVL2(REPLACE ('//', '//', ''), '(・ω・U)null以外', '(・ω・`U)null') 
FROM dual 
WHERE
  REPLACE (REPLACE (' テスト ', ' ', ''), ' ', '') = 'テスト' 
  AND REPLACE ('//', '//', '') is null

MySQL

mysql> SELECT ISNULL(REPLACE ('//', '//', '')) as a;
+---+
| a |
+---+
| 0 |
+---+

※REPLACE結果は空文字扱い

PostgreSQL

SELECT NULLIF(REPLACE ('//', '//', ''), '') ⇒ null

※REPLACE結果は空文字扱い

DB2

SELECT COALESCE(REPLACE ('//', '//', ''),'0') FROM SYSIBM.DUAL ⇒ 空文字

※REPLACE結果は空文字扱い

SQLite3

SELECT ifnull(REPLACE ('//', '//', ''),'0')  ⇒ 空文字

※REPLACE結果は空文字扱い

SQL Anywhere

SELECT ifnull(REPLACE ('//', '//', ''),'0', '1') ⇒ 1

※REPLACE結果は空文字扱い

まとめるまでもないがまとめると

RDBMS null == empty String
SQL Server false
Oracle true
MySQL false
PostgreSQL false
DB2 false
SQLite3 false
SQL Anywhere false

とまあ、Oracleだけがぽっかりと奇妙な挙動をしてくれている。 他のDBも概ねこうなのだろうな......。

ちなみに

https://docs.oracle.com/cd/E16338_01/server.112/b56299/sql_elements005.htm

Oracle Databaseは現在、長さが0(ゼロ)の文字値をNULLとして処理します。ただし、この処理はOracleの今後のバージョンでも継続されるとはかぎらないため、空の文字列をNULLとして処理しないことをお薦めします。

とあるから上記非推奨なんじゃないかとは思う。 だったら空文字でもtrueにするようにすればよかったと思うのだが......

2017年になっても上記の挙動は変わってないのがよくわかる事案だった。

退職のお知らせ

2013年より約4年間勤めておりました会社を2017年8月31日をもって退職することとなりました。 同時に渋谷を離れ故郷に戻り、同じようにITの仕事に携わることに相成りました。

在職中は主にビックデータの解析ツールの開発や各種フレームワークを用いた開発、保守運用など様々な業務に携わることができました。 また、mongoDBやRedisといったNoSQLをがっつり触れたこと、 MySQLPostgreSQLのバックアップやリストアといった普段なかなかやれることがない事象に対処することができたこと、 進捗管理についても線引きからじっくり携わる事ができたこと、 など、非常に面白いことに携わる事ができたのは非常に幸運であったと思っています。

次は東京を離れ故郷で地方のITに携わる事になります。 同じIT業界とはいえ、東京と地方ではまた変わってくると思うので、その違いに早くなじめるように頑張ろうと思います。

最後になりますが、東京でお会いした皆さまには本当にお世話になりました。 少し離れてしまいますが、今後ともTwitter含めちょくちょく発信はし続けていくつもりですので、どうぞよろしくお願いいたします。

input type="number" 用のmaxlength指定処理を作ってみた。

こんな感じで

$('input[type="number"]').on('input', function () {
    var regex = new RegExp(/^[0-9]+$/);
    var regexDecimal = new RegExp(/^([1-9][0-9]*|0)(\.[0-9]+)?$/);
    if ($(this).val().length > $(this).attr('maxlength')) {
        $(this).val($(this).val().slice(0, $(this).attr('maxlength')));
    } else if (parseInt($(this).val(), 10) != 0 && !parseInt($(this).val(), 10)) {
        $(this).val('');
    } else if (parseInt($(this).val(), 10) < parseInt($(this).attr('min'), 10)) {
        $(this).val($(this).attr('min'));
    } else if (parseInt($(this).val(), 10) > parseInt($(this).attr('max'), 10)) {
        $(this).val($(this).attr('max'));
    } else if (typeof $(this).attr('decimal') === "undefined" && !regex.test($(this).val())){
        $(this).val(parseInt($(this).val(), 10));
    } else if (typeof $(this).attr('decimal') !== "undefined" && !regexDecimal.test($(this).val())){
        $(this).val(parseFloat($(this).val()));
    }
});

今のところいい感じには動いてる。

FuelPHPのcoreのerrorhandler.phpがErrorhandlerを継承していない?

未だにこれの謎が解けぬ……なんなのだろう? ネームスペースの問題だとは思うが……

PHP Fatal error:  Uncaught Exception: File "COREPATH/classes/errorhandler.php" does not contain class "Fuel\Core\Errorhandler" in /hoge/fuel/core/classes/autoloader.php:397
Stack trace:
#0 /hoge/fuel/core/classes/autoloader.php(236): Fuel\Core\Autoloader::init_class('Fuel\\Core\\Error...', '/vagrant_data/i...')
#1 [internal function]: Fuel\Core\Autoloader::load('Fuel\\Core\\Error...')
#2 [internal function]: spl_autoload_call('Fuel\\Core\\Error...')
#3 /hoge/fuel/core/classes/autoloader.php(247): class_alias('Fuel\\Core\\Error...', 'Errorhandler')
#4 [internal function]: Fuel\Core\Autoloader::load('Errorhandler')
#5 /hoge/fuel/core/bootstrap.php(71): spl_autoload_call('Errorhandler')
#6 [internal function]: PHPUnit_Util_Fileloader::{closure}()
#7 {main}
  thrown in /hoge/fuel/core/classes/autoloader.php on line 397
PHP Stack trace:
PHP   1. {main}() /usr/local/bin/phpunit:0
PHP   2. PHPUnit_TextUI_Command::main() /usr/local/bin/phpunit:583
PHP   3. PHPUnit_TextUI_Command->run() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:118
PHP   4. PHPUnit_TextUI_TestRunner->doRun() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:188

Fatal error: Uncaught Exception: File "COREPATH/classes/errorhandler.php" does not contain class "Fuel\Core\Errorhandler" in /hoge/fuel/core/classes/autoloader.php on line 397

Call Stack:
    0.0009     495536   1. {main}() /usr/local/bin/phpunit:0
    0.2010   10224488   2. PHPUnit_TextUI_Command::main() /usr/local/bin/phpunit:583
    0.2010   10227648   3. PHPUnit_TextUI_Command->run() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:118
    1.4838   14664928   4. PHPUnit_TextUI_TestRunner->doRun() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:188

Exception: File "COREPATH/classes/errorhandler.php" does not contain class "Fuel\Core\Errorhandler" in /hoge/fuel/core/classes/autoloader.php on line 397

Call Stack:
    0.0009     495536   1. {main}() /usr/local/bin/phpunit:0
    0.2010   10224488   2. PHPUnit_TextUI_Command::main() /usr/local/bin/phpunit:583
    0.2010   10227648   3. PHPUnit_TextUI_Command->run() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:118
    1.4838   14664928   4. PHPUnit_TextUI_TestRunner->doRun() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:188
   86.1126   41742880   5. PHPUnit_Util_Fileloader::{closure:/hoge/fuel/core/bootstrap.php:34-72}() /hoge/fuel/core/bootstrap.php:0
   86.1129   41743200   6. spl_autoload_call() /hoge/fuel/core/bootstrap.php:71
   86.1129   41743240   7. Fuel\Core\Autoloader::load() /hoge/fuel/core/bootstrap.php:71
   86.1153   41743440   8. class_alias() /hoge/fuel/core/classes/autoloader.php:247
   86.1154   41743488   9. spl_autoload_call() /hoge/fuel/core/classes/autoloader.php:247
   86.1154   41743536  10. Fuel\Core\Autoloader::load() /hoge/fuel/core/classes/autoloader.php:247
   86.1154   41743632  11. Fuel\Core\Autoloader::init_class() /hoge/fuel/core/classes/autoloader.php:236

今更ながら改めてJavaのLongの比較を試してみた。

ソースレビューをしていた時にふと気づいたこと

  Long a = 1L;
  Long b = 1L;

があったとして、このLong値が一致するかどうかというのを

  if (a == b) {}; // 1.
  if (a.equals(b)) {}; // 2.

と比較方法が書く人によってバラバラなことに気付く。
まあ、ラッパークラスなので2.の方がいいというのはいいんだが、1.でも問題なく動いていたりするのでどうにも指摘しにくい。
というより自分自身Longの場合に上記の違いをちゃんと学んどこうと思ったのでちょっと試してみた。

まず作ったのはこんなコード

public class LongTest {

    public static void main(String[] args) {
        Long a = 1L;
        Long b = Long.valueOf("1");

        // a と b の比較
        if (a == b) {
            System.out.println("[a == b] true");
        } else {
            System.out.println("[a == b] false");
        }
        if (a.equals(b)) {
            System.out.println("[a.equals(b)] true");
        } else {
            System.out.println("[a.equals(b)] false");
        }
    }

}

で実行結果

[a == b] true
[a.equals(b)]  true

とどっちもtrueになったのでなんともしっくりこない。

とこんなことをコードと共にしっくりこないことを呟いたところ以下の指摘をいただく。

実際Longのコードを見てみると確かに

    public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }
    
    private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }

とある。なるほどな。

.longValue()でプリミティブ型に変換して比較する、という指摘もいただいたので2種類試してみる。

まず第1弾。値を1でやってみる。

public class LongTest {

    @SuppressWarnings("unused")
    public static void main(String[] args) {
        Long a = 1L;
        Long b = Long.valueOf("1");
        Long c = new Long(1L);
        Long d = null;

        // a と b の比較
        if (a == b) {
            System.out.println("[a == b] true");
        } else {
            System.out.println("[a == b] false");
        }
        if (a.equals(b)) {
            System.out.println("[a.equals(b)] true");
        } else {
            System.out.println("[a.equals(b)] false");
        }

        // a と c の比較
        if (a == c) {
            System.out.println("[a == c] true");
        } else {
            System.out.println("[a == c] false");
        }
        if (a.equals(c)) {
            System.out.println("[a.equals(c)] true");
        } else {
            System.out.println("[a.equals(c)] false");
        }

        // b と c の比較
        if (b == c) {
            System.out.println("[b == c] true");
        } else {
            System.out.println("[b == c] false");
        }
        if (b.equals(c)) {
            System.out.println("[b.equals(c)] true");
        } else {
            System.out.println("[b.equals(c)] false");
        }

        // .longValue() をつける
        if (a.longValue() == c.longValue()) {
            System.out.println("[a.longValue() == c.longValue()] true");
        } else {
            System.out.println("[a.longValue() == c.longValue()] false");
        }
        if (b.longValue() == c.longValue()) {
            System.out.println("[b.longValue() == c.longValue()] true");
        } else {
            System.out.println("[b.longValue() == c.longValue()] false");
        }

        // a と null の比較
        if (a == d) {
            System.out.println("[a == d] true");
        } else {
            System.out.println("[a == d] false");
        }
        if (a.equals(d)) {
            System.out.println("[a.equals(d)] true");
        } else {
            System.out.println("[a.equals(d)] false");
        }
    }

}

で実行結果

[a == b] true
[a.equals(b)]  true
[a == c] false
[a.equals(c)]  true
[b == c] false
[b.equals(c)]  true
[a.longValue() == c.longValue()] true
[b.longValue() == c.longValue()] true
[a == d] false
[a.equals(d)] false

なるほど確かにnewするとfalseになるな。

で第2弾。値を128でやってみる。

public class LongTest2 {

    public static void main(String[] args) {
        Long a = 128L;
        Long b = Long.valueOf("128");
        Long c = new Long(128L);

        // a と b の比較
        if (a == b) {
            System.out.println("[a == b] true");
        } else {
            System.out.println("[a == b] false");
        }
        if (a.equals(b)) {
            System.out.println("[a.equals(b)]  true");
        } else {
            System.out.println("[a.equals(b)] false");
        }

        // a と c の比較
        if (a == c) {
            System.out.println("[a == c] true");
        } else {
            System.out.println("[a == c] false");
        }
        if (a.equals(c)) {
            System.out.println("[a.equals(c)]  true");
        } else {
            System.out.println("[a.equals(c)] false");
        }

        // b と c の比較
        if (b == c) {
            System.out.println("[b == c] true");
        } else {
            System.out.println("[b == c] false");
        }
        if (b.equals(c)) {
            System.out.println("[b.equals(c)]  true");
        } else {
            System.out.println("[b.equals(c)] false");
        }

        // .longValue() をつける
        if (a.longValue() == c.longValue()) {
            System.out.println("[a.longValue() == c.longValue()] true");
        } else {
            System.out.println("[a.longValue() == c.longValue()] false");
        }
        if (b.longValue() == c.longValue()) {
            System.out.println("[b.longValue() == c.longValue()] true");
        } else {
            System.out.println("[b.longValue() == c.longValue()] false");
        }

    }

}

で実行結果

[a == b] false
[a.equals(b)]  true
[a == c] false
[a.equals(c)]  true
[b == c] false
[b.equals(c)]  true
[a.longValue() == c.longValue()] true
[b.longValue() == c.longValue()] true

なるほどたしかにnewしなくても == も通らなくなったね。

これを踏まえ処理を見直してみたところ、今回の場合==でも問題が発覚しなかったのは、Longで型を指定しているにも関わらず値が100以下がほとんどだったためという事もわかった。
まあ、バグの温床になってしまうのでequalsを使うように修正。
ラッパークラスがプリミティブ型とおなじような比較をある程度サポートしてくれているとはいえ、安易にそれに頼ってはいけないと改めて気づかされた次第。