NginxのCache設定

Nginx + SpringBoot の構成で環境を構築している。

まだ、静的ファイル(.css,.js,.jpg,.gif,.map) はCDNを使わない感じでなんとでもなる規模だが、アクセス数が増えていくと、ちょっとSpringBootだけだとレスポンスが遅くなってきたので、対策する。

静的ファイルはSpringBootで返して、Nginxでキャッシュする設定にする。

SpringBootは設定は特になし。

Nginxに下記の設定を行う。


server { location / { // 省略 proxy_ignore_headers Cache-Control Expires; proxy_cache cachezone; proxy_cache_valid 404 5m; // 省略 add_header X-Cache $upstream_cache_status [always]; } }

proxy_ignore_headers Cache-Control Expires; この設定が必要です。

キャッシュがヒットしてるかどうかは、add_header で確認できますが、HTTPステータスが正常系しか動作しません。
add_headeralways をつけると、どのHTTPステータスでもでてくので、確認できます。

このようにすると、ある程度の規模までは、CDN使わずに、サクサクいけます。
最近ではHTTP/2でコネクション数の上限もほとんど気にしなくていいですし。

Intellijでdebug実行すると warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

ブートストラップクラスパスが追加されているため、共有はブートローダークラスでのみサポートされます

ということらしいが、intellijでしか警告がでない。

Intellij Idea > preference > Build, Execution,Deployment > Async Stack TraceInstrumenting agent をunlockすれば、でなくなる。

intellijでdebugするとこの警告がでてくる warning: forcing TieredStopAtLevel to full optimization because JVMCI is enabled

OpenJDK 64-Bit GraalVM CE 19.3.0 warning: forcing TieredStopAtLevel to full optimization because JVMCI is enabled

JVMCIが有効になっているため、TieredStopAtLevelを完全に最適化する こういうことらしいが、gradle bootRun では発生しない。Intellijで発生する。

IntelliJの設定かなにかだろうと思い設定を確認

Run/Debug Configurations の中に Enable launch optimization があり、これをunlockすると警告がでなくなった

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

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をかけるような気がする

spring webfluxでfile upload

SpringBoot webfluxでは、ファイルアップロードはMultiPartで取得できないので、探す。
PartFileというのがよかったでこちらで実験する。

ソースはこちら

Javaはこちら

package net.ksasaki.springboot.example.controller;

import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Mono;

import java.io.File;

@Controller
@RequestMapping("")
public class SampleController {

    @RequestMapping("")
    public String index() {
        return "index";
    }

    @ResponseBody
    @PostMapping(value = "/form", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Mono form(@RequestPart("part") FilePart part) {

        File file = new File(part.filename());
        part.transferTo(file);

        return Mono.empty();
    }
}

HTMLはこちら

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="/form" enctype="multipart/form-data" method="post">
    <input type="file" name="parts">
    <button type="submit">送信</button>
</form>
</body>
</html>

無事取得できたので、メモ。

SpringBootアプリをjibでDocker化する

Google製のJavaをdocker化してくれるアプリでSpringBootのデモアプリをDocker化する

buildscript {
    ext {
        springBootVersion = '2.0.3.RELEASE'
    }
    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:0.9.8"
    }
}

group = 'net.kosasaki.example'
version = '0.0.1-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'idea'
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')
        compileOnly('org.projectlombok:lombok:1.18.0')
        compile 'mysql:mysql-connector-java:6.0.6'  
}

jib {
        from {
                image = 'adoptopenjdk/openjdk8:alpine-slim'
        }
        to {
                image = 'sample/jib-demo-app'
        }
        container {
                jvmFlags = ['-Xms512m', '-Xdebug']
                mainClass = 'net.kosasaki.example.WebApplication'
                args = []
                ports = ['8080/tcp']
                useCurrentTimestamp = true
        }

}

build.gradleはこんな感じ。

$ ./gradlew jibDockerBuild

docker images でイメージを確認する

$ docker images
REPOSITORY                                                         TAG                 IMAGE ID            CREATED             SIZE
sample/jib-demo-app                                                latest              891911ccd0de        About a minute ago   133MB

イメージ作成できてました。簡単すぎる。

イメージを実行して確認する

$ docker run --rm -it -p 8080:8080 --name jib-demo-app sample/jib-demo-app  
Picked up JAVA_TOOL_OPTIONS: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.3.RELEASE)

2018-08-05 16:18:36.344  INFO 1 --- [           main] net.kosasaki.example.WebApplication      : Starting WebApplication on 115367cada6f with PID 1 (/app/classes started by root in /)

確認できたので、OKとする

実にカンタン。これからはコンテナの世界ですねー。

にしても、alpine-slimで133MBとかJavaはサイズが大きいなー。
Java11とかでサイズ小さくなるといいなー。