Spring webfluxでGETパラメータが長いときの ステータス413(Request Entity Too Large)の対処

Spring webfluxでGetパラメータが長いときに、HTTPステータス413(Request Entity Too Large)が発生して少々ハマった。

application.yamlとかで解決はできない感じで下記ソースにて解決

なお、SpringBoot@2.0.7

@Component
public class NettyServerCustomize implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
    @Override
    public void customize(NettyReactiveWebServerFactory factory) {
        factory.addServerCustomizers(options -> {
            options.maxInitialLineLength(1024 * 1024);
        });
    }
}

この記事書いてて、SpringBootのバージョンあげないとなーって思いました

Mockitoでprivateメソッドのテストっぽいこと

ReflectionTestUtilsを使用する

fieldの場合

ReflectionTestUtils.setField を使う

public Sample SampleRepositoryImpl{
    private String parameter;
}


@Runwith(SpringRunner.class)
class SampleRepositoryImplTest(){

            @InjectMocks
            private SampleRepositoryImpl sampleRepository

            @Before
            public void setUp(){
                    ReflectionTestUtils.setField(sampleRepository,"parameter","local");
            }
}

上記のような感じで、差し替えができる。とても簡単。staticフィールドもできるもよう。

methodの場合

private methodをテストしたいときは、これで結果が取れるので、Assertiosでチェックしてやればいい。

ReflectionTestUtils.invokeMethod を使う

public Sample SampleRepositoryImpl{
        private Sring sampleMethod(){
             return "sample";
        }
}


@Runwith(SpringRunner.class)
class SampleRepositoryImplTest(){

            @InjectMocks
            private SampleRepositoryImpl sampleRepository

            @Test
            public void privateMethodTest(){
                    ReflectionTestUtils.invokeMethod(sampleRepository,"sampleMethod", null);
            }
}

staticメソッドもできるもよう。

まとめ

MockitoでprivateメソッドでテストするのはできなくてPowerMock使えって書いてあるけど、バージョンとか意識しないといけないの面倒だから、Mockitoでできないかなって調べたら、このくらいのことはできるので、わざわざライブラリ追加しなくていいかなって感じでした。

JavaのFunctionの合成はかなり便利

Java8から導入されたFunctionインターフェースはかなり便利な機能で多用している。特に関数合成が便利なので、ぜひ利用してほしい。

Functionインターフェース

define

Function<String, Integer> string2Integer = s -> Integer.valueOf(s);

Functionインターフェースではジェネリクスの1つめが引数、2つめが戻り値となっている
いつものJavaと比べるとちょっと変ですよね

use

s.apply("2");

これでOK。apply(引数)って感じで渡せばいいだけです。

他のパラメータも渡したい

これだと変数1個しか渡せなくて不便に感じる人もいるかとおもいます。変数宣言だと無理なのですが、メソッドとしてもFunctionインターフェース使えるのでこれを使うべしかなと思います。

define

Function<String, Integer> s(Integer param) {
    return s -> {
            return Integer.valueOf(s) + param;
        }
}

こんな感じになります。lambda式な感じになるので、違和感なく使えるかなと。メソッドの引数にパラメータを渡す感じです。

use

s(1).apply("2")

こんな感じで使います。 s(1) これでFunction<T,R>の戻り値を返却して、それをapplyする感じにしています。

合成

Functionは合成機能が標準であるので、これがとても便利です。
AやってBやってCやってみたいなのが、関数として組み合わせられて、状態依存もしにくいのでいいかなと。

define


Function<String, Integer> string2Integer = sVal -> Integer.valueOf(s); Function<String, Integer> sMethod(Integer param) { return s -> { return Integer.valueOf(s) + param; } }

先程の変数宣言とメソッド宣言を名前変えておきます。

use


sVal.andThen(sMethod(3)).apply("2")

こんな感じで使います。
andThenで関数と関数をくっつけます。(composeっていうのもあります)
実行順は andThencomposeで異なります。

andThenは A.andThen(B) だとすると A->Bの順
composeはA.compose(B)だとすると B->Aの順

で実行されます。

その他

関数合成ができてからとても便利になりました。
基本的に操作したい対象とパラメータはわけた方がよくて、パラメータ等はメソッドの引数で渡すのがいいと思います。
操作対象をFunctionインターフェースの方に渡すのがいいと思いました。

Vue.jsでファイルをdrag and dropする

Vue.jsでファイルのdrag and drop をするときにdomイベントをどうやって渡すんだ?ってなったので、メモ。

公式のドキュメントに書いてあったので イベントハンドラ でそれを参照する。

$event を使うことででDOMイベントを渡すことができる。

サンプルコードは下記

<template>
  <div>
      <h1>drag and drop</h1>
            <label></label
      <div class="drop_area" @dragover.prevent @dragleave.prevent @drop.prevent @drop="drop($event)">

      </div>
  </div>
</template>

<script>
  export default {
    name: 'main-page',
    data () {
      return {}
    },
    methods: {
      drop (event) {
        // 処理を書く
      }
    }
  }
</script>

<style>
.drop_area {
  border: 5px solid gray;
  background: gray;
  width:  400px;
  height: 400px;
}
</style>


ブラウザはファイルをdrag and drop するとデフォルトの処理が発火してしまうので、下記を定義しておけば、それを抑止して、カスタムな処理ができるようになる。

@dragover.prevent @dragleave.prevent @drop.prevent

うん。簡単。

Intellijのgoogle-java-format plugin を外した.

ちょっと思うところがあって、google-java-format plugin を外した。
このプラグインがあると、結構強制力が強くて、indent4 にしたいときにできなかったり、import文を変えたかったりしたときにできなかったりとか・・・あったので、外しました…

ただそれだけ…

https://plugins.jetbrains.com/plugin/8527-google-java-format

Java8の@FunctionalInterfaceを使う

関数インターフェースのPredicateとかFunctionがあるのであんまり使わないけど、引数とか3つ以上のものはないのでつくる必要がある。

基本的に抽象メソッドを1つしかもっていなければOK。defaultメソッドはいくつもっていてもOKです。(ラムダ式のときに何を使うかわからなくなっちゃうからかなと)

@FunctionalInterface
interface SampleInterface {
      boolean filter(String a);
}

こんな感じでカンタンに作れる

使うときはこんな感じでカンタンに使える。

SampleInterfacle sampleFilter = s -> "a".equals(s);
sampleFilter.filter("a");

なんかもうカンタン過ぎますね

ちなみに引数3つは、こんな感じ

@FunctionalInterface
interface SampleInterface {
      boolean filter(String a, String b, String c);
}

SampleInterfacle sampleFilter = (s, t , u)  -> "a".equals(s);
sampleFilter.filter("a","b","c");

こんな感じで使える。なんかもうカンタン過ぎますね。みんなもっと使いましょう!

Javaの関数インターフェース再入門

Javaのよく使ってる関数インターフェース群をまとめようかとおもう。
現場ごとに説明のためのドキュメントを書くのがしんどくなってきたからというのが本音かも。

predicate

断言するって意味。戻り値はBoolean。
引数1つとって

Predicate<String> matchA = s -> s.matches(".*a.*");  // 'a'があればtrue
Predicate<String> matchB = s -> s.matches(".*b.*"); // 'b'があればtrue
Predicate<String> noA = matchA.negate(); // 'a'があったらfalse

// 合成
Predicate<String> ab = matchA.and(matchB); // 'a'と'b'があったらtrue
Predicate<String> ab = matchA.or(matchB); // 'a'または'b'があったらtrue

実行はtest(T)でできる。カンタン。streamのfilterとかで使える。テストもカンタン。

matchA.test("a");

これで実行できる。

ちなみに2つ引数とることもできる

BiPredicate<String,String> matchAB = (s,t) -> s.matches(".*a.*") && t.matches(".*a.*");

suppplier

供給するって意味

引数なし、戻り値あり。引数ないけど、それ単体で何かが発生する。
なにかと組み合わせてデータ流す感じかな

Supplier<String> supplier = () -> {
     SecureRandom random = new SecureRandom();
     return String.valueOf(random.nextInt());
};

ランダムintを返却する

supplier.get()

consumer

消費するって意味。
実際に引数とって、そのあと処理する。戻り値なし。

下記のような感じ

Consumer<String> show = s -> System.out.println(s);

下記のように使う

s.accept("s");

2つ引数とることも可能

BiConsumer<String,Integer> biShow = (s,i) -> {
    System.out.println(s);
    System.out.println(i);
};

Function

機能として使える関数インターフェース

引数と戻り値を持てる。
引数2つまで持てるものもある。(BiFunction)

下記のように定義できる

Function<Integer,String> exchangeString = t -> String.valueOf(t);
BiFunction<Integer,Integer,String> biExchange = (t,u) -> String.valueOf(t + u);

下記のように使える

exchangeString.apply(1);
biExchange.apply(1,2);

springboot devtools のauto restart を疑似高速化する

きっかけ

springbootとintellijなどIDE使ってると、コードの補完はめちゃされるし、Javaって便利だなーとか先人の方たちありがとう!って気持ちでコード書いてるんですけど、コード変更時に変更内容がHotswapで反映されるならいいんですけど、そうじゃないとrestartがかかって、プロジェクトが少し大きくなってくると15秒くらいまたされるのでもっと短縮できないかなーって思いました。

やったこと

DIの対象になるクラスを全部遅延ロード(lazyload)してしまえばいいかと思い、それを実装してみました。
本番環境でやるのはリスキーなので、local環境のみで行うようにしています。

@Profile("local")
@Configuration
public class LazyInitBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            beanFactory.getBeanDefinition(beanName).setLazyInit(true);
        }
    }
}

これで15秒から5秒後半くらいになりました!最高

Started WebApplication in 14.565 seconds (JVM running for 16.236)
↓
Started WebApplication in 5.759 seconds (JVM running for 6.871)

リスク

初期化が遅らせているので、初期化順でなにかを行う処理を挟み込んでたりしたら影響がありますが、今日そんなコード書いてる人なんてあんまりいないだろうし、local環境だけなんでまぁ影響ないかなと。

spring-context-indexerと組み合わせているとちょっと動かないケースがあってハマることがあるので、オフにできるならオフにすること。
まだなんでかはわかってないです。

まとめ

これでサクサク開発できるので、しばらく試してみようかと思います

mybatisでカスタムクエリをxmlで書くのは辛いので、freemarker templateにする

mybatisでカスタムでクエリ書くときに、標準だとxml形式で書かなきゃいけなくて、結構しんどい。

カンタンに書くと下のような感じ

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="jp.sample.MapperCustom">
  <delete id="deleteAll" parameterType="map">
    delete from ${table}
  </delete>

  <insert id="bulkInsert" parameterType="map">
    insert into ${table}
    (code)
    values
    <foreach item="v" collection="list" separator=",">
      (#{v})
    </foreach>
  </insert>
</mapper> 

つらいポイント
– xml形式なので、余計なものが先頭に来てつらい
– 繰り返しがつらい

なんかたくさんあるかと思いきや、あんまり挙げられなかったのだが、とにかくXMLというのが辛い。不等号もxml形式なので、CDATAを利用してかかないければならないのとか、そうしないなら、エスケープしないといけないのが辛かった。

Groovyとかkotlinだとmulti line strings が使用できる話だが、Groovyだと大きいJarを入れないとだし、kotlinだとnull値が。。。みたいな問題に悩まされるんじゃないかという気がしたので、mybatis freemarker templateを使う方法を選択した。

使い方

ライブラリの追加

gradleのdependenciesに下記を追加

compile("org.mybatis.scripting:mybatis-freemarker:$mybatisFreemakerVersion")

freemarkerテンプレートを置き場指定

src/main/resources 直下にmybatis-freemarker.propertiesのファイルを設置する
内容は、mybatis-freemarkerを使う際にどのディレクトリを参照するかなので、classpath内の任意のディレクトリパスを記述する。

basePackage=mybatis/sql

freemarker templateにクエリを書く

src/main/resources/mybatis/sql/sample.ftl

select
  name
from
  sample
where
  id in (${ids?join(", ")})
  and
  name = <@p name="n">

(${ids?join(", ")})とか<@p name="n"> とかのfreemarker templateでかけるのは嬉しい。xml形式でない分可読性も高くなると思うし、使い勝手がよくなる。

mapperインターフェース内にfreemarker templateの呼び出しを書く

@Lang(FreeMarkerLanguageDriver.class)
@Select("sample.ftl")
List<Name> findNamesByIds(@Param("n") String name,@Param("ids") List<Integer> ids);

以上のように書けば、実行できる。なんかかなりスッキリしたよい感じになる。デメリットとしては、1SQLで1ファイル使うので結構ファイルの数は増える。ディレクトリ等を使えば、スッキリはすると思うので、大したデメリットにはならないと思う。

xmlを敬遠しがちだったが、こっちならバリバリSQLをかけるような気がする

SpringReactive(webflux)でAOPする際に@EnableReactiveMethodSecurityの@hasAuthoritiesを使うとうまくいかないのを回避する

下記のようにcontrollerでのリクエストのログを認証情報付きで出力しようとする。

    @AfterReturning(value = "execution(* xxx.xxx.controller..*.*(..))")
    public void execNormalControllerMethod(JoinPoint jp) {
            /// ログ出力処理
    }

しかし、下記のように、@PreAuthorizeするとエラーになる。

@RestController
@RequestMapping("sample")
public class SampleController {
       @RequestMapping
       @PreAuthorize("hasAuthority('ADMIN_ROLE')")
        public Mono index(@AuthenticationPrincipal UserDetails useDetails) {
             return Mono.empty();
        }
}

エラーは下記

No MethodInvocation found: Check that an AOP invocation is in progress, and that the ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor!

下記にて対処可能
@EnableReactiveMethodSecurityのOrderを最上位にすれば動作する

@EnableReactiveMethodSecurity(order = Ordered.HIGHEST_PRECEDENCE)

以上、設定して再起動すればOK