読者です 読者をやめる 読者になる 読者になる

Fight the Future

何かを始めたら、半分成功したのと同じ

InfoQ 日本語サイトにて翻訳活動を始めました

java 翻訳 コミュニティ

初めての翻訳が本日公開されました。

www.infoq.com

Java関連の翻訳を中心に、英語力の向上と技術情報の収集を兼ねて、1か月に1つ2つ翻訳します!

A simple example of Gauge + Selenide tests #gauge #selenide

java

I tweeted as below, I received reply from Selenide creator.

次のようにツイートしたところ、Selenideの作者からリプライをもらいました。

So, I publish a simple example of Gauge + Selenide tests in GitHub.

なので、GitHubに本当にシンプルなGauge + Selenideのサンプルを上げました。

github.com

hsdisをMacでビルドする

jvm

Java Day Tokyo 2016に出たところ、hsdisをもうProject Kenaiからダウンロードしてはいけない、古いビルドだから新しいCPUに対応していないということを聞きました。 そのため、帰宅してすぐビルドしました。簡単でした。

まずhsdisディレクトリに移動します。

$ cd jdkd9-b81/hotspot/src/share/tools/hsdis

binutilsをダウンロードします。 http://ftp.gnu.org/gnu/binutils/で最新のバージョン番号を調べてからwgetします。

$ wget http://ftp.gnu.org/gnu/binutils/binutils-2.26.tar.gz                                                                                                                                                        jdk1.8.0_65 ruby-2.0.0
--2016-05-25 15:26:52--  http://ftp.gnu.org/gnu/binutils/binutils-2.26.tar.gz
Connecting to 10.5.81.33:3128... connected.
Proxy request sent, awaiting response... 200 OK
Length: 34832117 (33M) [application/x-gzip]
Saving to: 'binutils-2.26.tar.gz'

binutils-2.26.tar.gz                                       100%[==========================================================================================================================================>]  33.22M   256KB/s   in 2m 35s 

2016-05-25 15:29:28 (219 KB/s) - 'binutils-2.26.tar.gz' saved [34832117/34832117]

binutilsを解凍します。

$ tar xzf binutils-2.26.tar.gz 

makeします。

$ make BINUTILS=binutils-2.26 ARCH=amd64
[ -d build/macosx-amd64 ] || mkdir -p build/macosx-amd64
...
hsdis.c:137:1: warning: control reaches end of non-void function [-Wreturn-type]
}
^
hsdis.c:207:11: warning: enumeration value 'dis_noninsn' not handled in switch [-Wswitch]
  switch (itype) {
          ^
2 warnings generated.

2つ警告は出ましたが、makeできました。

$ ls -lat build/macosx-amd64/
total 4152
drwxr-xr-x   12 jyukutyo  staff      408  5 25 15:34 .
-rwxr-xr-x    1 jyukutyo  staff  1676824  5 25 15:34 hsdis-amd64.dylib
drwxr-xr-x   29 jyukutyo  staff      986  5 25 15:34 opcodes
drwxr-xr-x  107 jyukutyo  staff     3638  5 25 15:34 bfd
drwxr-xr-x   23 jyukutyo  staff      782  5 25 15:34 zlib
drwxr-xr-x   79 jyukutyo  staff     2686  5 25 15:33 libiberty
drwxr-xr-x    8 jyukutyo  staff      272  5 25 15:33 intl
-rw-r--r--    1 jyukutyo  staff   381609  5 25 15:33 Makefile
-rw-r--r--    1 jyukutyo  staff    23885  5 25 15:33 config.log
-rwxr-xr-x    1 jyukutyo  staff    31257  5 25 15:32 config.status
-rw-r--r--    1 jyukutyo  staff       13  5 25 15:32 serdep.tmp
drwxr-xr-x    3 jyukutyo  staff      102  5 25 15:32 ..

このhsdis-amd64.dylibをJava 8までなら$JAVA_HOME/jre/lib/amd64に、Java 9なら$JAVA_HOME/lib/amd64に配置します。

indyとASMを使ってコードを難読化できるんだって

java

そういえばJavaOne 2015で「Protecting Java Bytecode from Hackers with the InvokeDynamic Instruction 」というセッションに出ておもしろかったことを思い出しました。

そこでデモ用のものが紹介されていたので、今更ながら試してみました。

github.com

これは、クラスファイルを1つ入力にして、難読化したクラスファイルを1つ出力するものです。ここでの難読化というのは、invokevirtual、invokeinterface、invokestaticといったメソッド呼び出しを難読化することに目的を絞っています。

普通のHelloWorld。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

javapします。

$ javap -v HelloWorld
...
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

さきほどのGithubのプロジェクトをcloneしmvn packageするとIndyProtectorDemo-1.0.jarというJARファイルができます。これを使って$ java -jar IndyProtectorDemo-1.0.jar HelloWorld.class HelloWorld2.classを実行します。

$ javap -v HelloWorld2
...
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #19                 // String Hello World
         5: invokedynamic #37,  0             // InvokeDynamic #0:"242602059":(Ljava/lang/Object;Ljava/lang/Object;)V
        10: return
      LineNumberTable:
        line 3: 0
        line 4: 10
}
BootstrapMethods:
  0: #26 invokestatic "LHelloWorld;".bootstrap$0:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
    Method arguments:
      #27 182
      #29 java.io.PrintStream
      #31 println
      #33 (Ljava/lang/String;)V

invokevirtualがなくなってinvokedynamicを使うようになりました。BootstrapMethodsも作られています。

このライブラリの中身は、ASMを使ってバイトコードを操作しています。ASMのOpcodesを実装してブートストラップメソッドを生成しています。

public class BootstrapMethodGenerator implements Opcodes {
...
}

indyへの置換はASMのMethodVisitorのサブクラスを作って処理しています。

public class MethodIndyProtector extends MethodVisitor implements Opcodes {
    Handle bootstrapMethodHandle = null;
    SecureRandom rnd = new SecureRandom();
...    
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        boolean isStatic = (opcode == Opcodes.INVOKESTATIC);
        String newSig = isStatic ? desc : desc.replace("(", "(Ljava/lang/Object;");
        Type origReturnType = Type.getReturnType(newSig);
        Type[] args = Type.getArgumentTypes(newSig);
        for (int i = 0; i < args.length; i++) {
            args[i] = genericType(args[i]);
        }
        newSig = Type.getMethodDescriptor(origReturnType, args);
        switch (opcode) {
            case INVOKESTATIC: // invokestatic opcode
            case INVOKEVIRTUAL: // invokevirtual opcode
            case INVOKEINTERFACE: // invokeinterface opcode
                mv.visitInvokeDynamicInsn(String.valueOf(rnd.nextInt()), newSig, bootstrapMethodHandle, opcode, owner.replaceAll("/", "."), name, desc);
                if (origReturnType.getSort() == Type.ARRAY) {
                    mv.visitTypeInsn(Opcodes.CHECKCAST, origReturnType.getInternalName());
                }
            break;
            default:
                mv.visitMethodInsn(opcode, owner, name, desc, itf);
        }
    }

indyを使って、invoke*の呼び出しを取り除くことができました。これがどのように役立つかは僕には少し思いつきませんが…indyを使っているので、実行速度への影響はあまりないでしょうし、将来的なJVMの改善でパフォーマンスが向上する余地もありそうです。

Spring MVCでConversion失敗をまとめて処理したいときは@ModelAttributeを使うしかないのかな?

Spring java

Spring MVCでこういうコントローラメソッドがあるとする。

    @RequestMapping(path = "hoge")
    public String index(@RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date start, @RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date end) {
        return "hoge";
    }

start、endという日付はブラウザでテキストボックスに文字列として日付を入力するみたいな想定で、日付として無効な文字列を入力したときにエラーメッセージを出すような、よくある感じにしたい。

だけど、実際に無効な文字列を入れてsubmitしてみると、ステータスコード400になる。スタックトレースも出る。

Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.lang.String to type @org.springframework.web.bind.annotation.RequestParam @org.springframework.format.annotation.DateTimeFormat java.util.Date for value 'hoge'; nested exception is java.lang.IllegalArgumentException: Invalid format: "hoge"
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41) [spring-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192) [spring-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:173) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:108) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:64) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:47) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.validation.DataBinder.convertIfNecessary(DataBinder.java:688) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:107) [spring-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ... 49 common frames omitted
Caused by: java.lang.IllegalArgumentException: Invalid format: "hoge"
    at org.joda.time.format.DateTimeFormatter.parseDateTime(DateTimeFormatter.java:866) [joda-time-2.1.jar:2.1]
    at org.springframework.format.datetime.joda.DateTimeParser.parse(DateTimeParser.java:49) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.format.datetime.joda.DateTimeParser.parse(DateTimeParser.java:33) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:194) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.format.support.FormattingConversionService$AnnotationParserConverter.convert(FormattingConversionService.java:311) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:35) [spring-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ... 56 common frames omitted

org.springframework.web.method.support.InvocableHandlerMethodの実装はこうなってる。

   private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {

        MethodParameter[] parameters = getMethodParameters();
        Object[] args = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            MethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
            args[i] = resolveProvidedArgument(parameter, providedArgs);
            if (args[i] != null) {
                continue;
            }
            if (this.argumentResolvers.supportsParameter(parameter)) {
                try {
                    args[i] = this.argumentResolvers.resolveArgument(
                            parameter, mavContainer, request, this.dataBinderFactory);
                    continue;
                }
                catch (Exception ex) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
                    }
                    throw ex;
                }
            }
            if (args[i] == null) {
                String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
                throw new IllegalStateException(msg);
            }
        }
        return args;
    }

ループでまわしていて、今回のようなConversionFailedExceptionも含めて例外が出た時点でキャッチしてリスローしているので、たとえばコントローラメソッドのすべての引数を変換してみて失敗したフィールドにはすべてエラーメッセージを出したい、という要件は実現できない。

メソッドの引数に@RequestParamをつけるパターンでは無理そうなので、別の方法である@ModelAttributeとBindingResultを使うしかなさそう。

    @RequestMapping(path = "fuga")
    @ResponseBody
    public String fuga(@ModelAttribute Fuga fuga, BindingResult result) {
        return "fuga";
    }
import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;

public class Fuga {

   @DateTimeFormat(pattern="yyyyMMdd")
    private Date start;
    @DateTimeFormat(pattern="yyyyMMdd")
    private Date end;

    // setters and getters...
}

こうすると、BindingResult#hasErrors()でConversion失敗があったかはわかるし、BindingResult#getAllErrors()で各失敗の詳細情報も取得できる。

Bean ValidationでSpring Expression Languageを使って相関バリデーションする

Spring java

このエントリで実装を見ていたとき、思いついた。 jyukutyo.hatenablog.com

Spring Expression Language(SpEL)でもバリデーションできそうだな〜と。Spring、Hibernate Validatorの利用が前提になってしまうけど。

@SpringELAssertアノテーションというのを作り、expressionの値にバリデーション処理を記述する感じ。

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ CONSTRUCTOR, METHOD })
@Retention(RUNTIME)
@Constraint(validatedBy = SpringELAssertValidator.class)
public @interface SpringELAssert {

    String message() default "{}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    String expression();

    /**
     * Defines several {@link SpringELAssert} annotations on the same executable.
     */
    @Target({ CONSTRUCTOR, METHOD })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        SpringELAssert[] value();
    }

}

このアノテーションは@ParameterScriptAssertと内容は同じです。で、バリデーション処理はSpringELAssertValidatorクラスに記述する。

import java.util.List;
import java.util.Map;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;

import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class SpringELAssertValidator implements ConstraintValidator<SpringELAssert, Object[]> {

    private Expression expression;

    @Override
    public void initialize(SpringELAssert constraintAnnotation) {
        ExpressionParser parser = new SpelExpressionParser();
        this.expression = parser.parseExpression(constraintAnnotation.expression());
    }

    @Override
    public boolean isValid(Object[] arguments, ConstraintValidatorContext context) {

        List<String> parameterNames = ((ConstraintValidatorContextImpl)context).getMethodParameterNames();

        Map<String, Object> bindings = getBindings(arguments, parameterNames );

        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
        evaluationContext.setVariables(bindings);

        Boolean result = this.expression.getValue(evaluationContext, Boolean.class);
        return result == null ? true : result.booleanValue();
    }

    private Map<String, Object> getBindings(Object[] arguments, List<String> parameterNames) {
        Map<String, Object> bindings = new HashMap()<>;

        for ( int i = 0; i < arguments.length; i++ ) {
            bindings.put( parameterNames.get( i ), arguments[i] );
        }

        return bindings;
    }
}

これもParameterScriptAssertValidatorクラスの内容を少し書き換えた。引数名とバリデーションする値をMapにできるので、このMapをSpELのコンテキストにvariableとしてセットする。expressionではvariableは"#name"で参照できる。

Variables can be referenced in the expression using the syntax #variableName. Variables are set using the method setVariable on the StandardEvaluationContext.

これで完成。コントローラはこうなる。

import java.util.Date;

import jp.furyu.voyager.newsp.spring.validation.SpringELAssert;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@Validated
public class HogeController {

    @RequestMapping(path = "hoge")
    @ResponseBody
    @SpringELAssert(expression = "#start.before(#end)")
    public String index(@RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date start, @RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date end) {
        return "hoge";
    }

}

@ParameterScriptAssertとほぼ変わらないけど、SpELで書けるのでここでいきなりJavaScriptを持ち出してくるよりはいいかな?