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でできないかなって調べたら、このくらいのことはできるので、わざわざライブラリ追加しなくていいかなって感じでした。

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

Javaアプリをdocker化してくれるjibを環境ごとに分岐する

Java(Spring)アプリをdocker化してくれるんだけど、環境ごとに分けたいことはあるので、パラメータを切り替えられるようにする。

buildscript {
    ext {
        springBootVersion = '2.0.3.RELEASE'
                jibVersion = "0.9.8"
    }
    repositories {
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:${jibVersion}"
    }
}

apply plugin: 'org.springframework.boot'
apply plugin: 'com.google.cloud.tools.jib'
    dependencies {
        compile('org.springframework.boot:spring-boot-starter-webflux')
        compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    }

/**
* jibの設定 ifで分岐する
*/
    if(project.build == 'product') {
        jib {
            from {
                image = 'adoptopenjdk/openjdk8:alpine'
            }
            to {
                image = 'sample/jib-demo-product-app'
            }
            container {
                jvmFlags = ['-Xms512m', '-Xdebug']
                mainClass = 'com.example.demo.DemoApplication'
                args = []
                ports = ['8080/tcp']
            }

        }
    } else {
        jib {
            from {
                image = 'adoptopenjdk/openjdk8:alpine'
            }
            to {
                image = 'sample/jib-demo-app'
            }
            container {
                jvmFlags = ['-Xms512m', '-Xdebug']
                mainClass = 'com.example.demo.DemoApplication'
                args = []
                ports = ['8080/tcp']
            }

        }
    }

上記のように設定し、gradleコマンドをキックするときに、引数を渡してあげればいい

$ ./gradlew -Pbuild=product

=================
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
sample/jib-demo-product-app   latest              4fe1925bb902        48 years ago        220MB

こんな感じで作成される。 48years ago ってでてるけど、なんなんだろ。まぁCREATEDだからいいか

他にもjibのタスク呼び出しとかあるが、JibExtensionとかいろいろ呼ぶの面倒そうなので、逃げでもある

Springのアノテーション(@Async)で非同期を使う

Springで非同期処理を簡単に使う方法のメモ

参考はこちら -> Spring MVC(+Spring Boot)上での非同期リクエストを理解する

Springデフォルトの非同期

Springは起動クラスにアノテーションをつけるだけで、非同期処理ができる

@EnableAsyncを起動クラスに追加する

Application.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAutoConfiguration
@EnableAsync // <- これ追加する
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }

}

これだけで、非同期のアノテーション(@Async)が使えるようになります。

@Asyncをつけたテキトーなクラスを作ってみます
SampleService.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@Async
public class TopServiceImpl {

    public String index() {

        try {
            for (int i = 0; i < 1000; i++) {
                Thread.sleep(10);
                log.info("{}",i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

下記のようにログがでてきました

2018-01-08 23:35:24.508  INFO 20519 --- [cTaskExecutor-1] com.tasktalk.service.TopServiceImpl      : 0

cTaskExecutor-1がでてきて、呼び出し回数ごとにこのスレッドが増えていきます。
これだけだと、スレッド数を管理できません。

スレッド数をSpringで管理する

WebMvcConfigurerAdapterを継承したクラスでメソッド2つ追加する

WebMvcConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ComponentScan
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {  // <- このメソッド追加
        configurer.setTaskExecutor(taskExecutor());
    }

    @Bean
    public AsyncTaskExecutor taskExecutor(){ // <- このメソッド追加
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(10);
        return executor;
    }
}

下記のようにログがでてきました

2018-01-08 23:30:53.612  INFO 20519 --- [ taskExecutor-1] com.tasktalk.service.TopServiceImpl      : 149

taskExecutor-1と表記されていて、設定したスレッドプールから使用されております。

spring boot devtools or spring loaded をIntellijで動かす

SpringBootで開発してるときにサクサク開発したい欲求があり、以前はEclipse + SpringBoot + SpringLoaded使ってて、かなりサクサク開発できてて、気持ちよかったんです。(アノテーションの変更は検知してもらえなかった記憶が)
現在、IntelliJ + SpringBoot devtools にした時に、IntelliJの内部処理?のおかげで上手く動かなかったので、動くようにする方法をいくつか。

IntelliJでspringboot-devtools or spring-loadedが動かない原因

↓これになります。

説明にあるように、IntelliJでSpringBootを起動した場合、起動中はビルドが走らないようになっています。
springboot-devtoolsもspring-loadedもバイナリの変更を検知してreloadしているので、これではビルドが走らないです。

解決策

IntelliJでFull Buildするマクロを仕込み、キーマップを設定する

  • Edit -> Macros -> Start Macro Recording をクリック

  • Build -> ReBuild Project をクリック
  • Edit -> Macros -> Stop Macro Recording をクリック
    • マクロ名をきめる
  • 上記で作成したマクロをkeymap に登録する

⌘ + F9 で強制ビルドする

ちょっとした変更なら、これでいける。
– Javaソース以外だとビルドかからないかも

コマンドラインからmaven or gradle で起動する

コマンドラインから起動します。

$ mvn spring-boot:run

or 

$ gradle bootRun

IntellJのビルドしない条件に引っかからないので、springboot-devtoolsもspring-loadedも動作します。