Hibernate Searchでソート機能を使用する

@SortableFieldを付けるとそのFieldに対してソート用のインデックスが作成されるようになる。

@Field(analyze = Analyze.NO)
@SortableField
private String description;

但し、ソート可能とするFieldはトークン化してはいけない。そのため、FieldをAnalyze.NOとするか、Normalizerを指定する必要がある。(AnalyzerとNormalizerは排他的なのでNormalizerを指定すると結果的にAnalyze.NOになっている)

@NormalizerDef(name = "novelSort", filters = @TokenFilterDef(factory = LowerCaseFilterFactory.class))
public class Novel extends BaseObject implements Serializable {

    @Field
    @Field(name = "descriptionSort", normalizer = @Normalizer(definition = "novelSort"))
    @SortableField(forField = "descriptionSort")
    private String description;

実は@SortableFieldを指定しなくてもソート自体は出来るがインデックスが無いので、パフォーマンスが悪い。

Sort sort = new Sort(new SortField("descriptionSort", SortField.Type.STRING))

FullTextQuery query = ...
query.setSort(sort);

Gradleでコードカバレッジのレポートを作成する

JaCoCoプラグインを使う。
https://docs.gradle.org/current/userguide/jacoco_plugin.html

build.gradleに下記を追記する。

plugins {
    id 'jacoco'
}

jacocoTestReport {
    reports {
        xml.enabled false
        csv.enabled false
        html.destination file("${buildDir}/jacocoHtml")
    }
}

これでタスクにjacocoTestReportを追加して実行すればレポートが作成されるようになる。
(デフォルトの出力先は$buildDir/reports/jacoco)

さらに、当プロジェクトではLombokを使用しているため、Lombokで自動生成されたコードをカバレッジの対象外にしたい。
プロジェクトのルートフォルダにlombok.configファイルを作成し、下記の通り記入することで対象外とすることが出来る。

lombok.addLombokGeneratedAnnotation = true

Spring Dataで戻り値をStreamにした場合は使用後にcloseすること

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-streaming

検索結果が格納されているStreamは使用後にcloseしないと駄目らしい。

return novelDao.findByUnreadTrueOrderByTitleAndNovelChapterId().collect(Collectors.toList());

上記を下記のように変更する必要がある。

try (Stream<Novel> novels = novelDao.findByUnreadTrueOrderByTitleAndNovelChapterId()) {
    return novels.collect(Collectors.toList());
}

Spring BootでLazyInitializationExceptionが発生する場合

fetch = FetchType.LAZYとしている場合、データベースを参照するときにSessionが切れていてLazyInitializationExceptionが発生することがある。

Caused by:
 org.hibernate.LazyInitializationException:
  failed to lazily initialize a collection of role:
   crawlerapi.entity.Novel.novelChapters, could not initialize proxy - no Session

そのような場合は、application.ymlに以下を追加する。

jpa:
  properties:
    hibernate:
      enable_lazy_load_no_trans: true

OpencsvでJava BeansとMappingする

Opencsvについては、リンク先を参照。
ここではアノテーションを使わずに、各列を各変数にマッピングする方法を紹介する。

・CSVを読み込む

try (InputStreamReader is = new InputStreamReader(multipartFile.getInputStream(), Constants.ENCODING);
        CSVReader reader = new CSVReaderBuilder(is).withSkipLines(1).build()) {
    ColumnPositionMappingStrategy strat = new ColumnPositionMappingStrategy<>();
    strat.setType(User.class);
    strat.setColumnMapping("username", "password", "firstName", "lastName", "email");

    CsvToBean<User> csv = new CsvToBean<>();
    csv.setCsvReader(reader);
    csv.setMappingStrategy(strat);
    return csv.parse();
} catch (IOException e) {
    // 適宜例外処理
} catch (IllegalStateException e) {
    // 適宜例外処理
}

InputStreamReaderについては、どこからファイルを取得するかによって変更する。

CSVReader reader = new CSVReaderBuilder(is).withSkipLines(1).build();

で、CSVReaderを作成する。1行目はヘッダー行のため、withSkipLines(1)で2行目から読み込むようにしている。

ColumnPositionMappingStrategyで、CSVファイルの各列をどのクラスのどの変数にマッピングするか指定出来る。
strat.setType(User.class)で、Userクラスにマッピングすることを指定している。
strat.setColumnMapping(“username”, “password”, “firstName”, “lastName”, “email”)で、1列目をUserクラスのusername変数へ、2列目をpasswordへのように指定していることになる。

CsvToBeanで、List<String[]>をList<User>に変換する。

・CSVに書き出す

try (OutputStreamWriter os = new OutputStreamWriter(response.getOutputStream(), Constants.ENCODING);
        CSVWriter writer = new CSVWriter(os)) {
    // ヘッダー行を追加
    writer.writeNext(new String[] {"ユーザ名","パスワード","名字","名前","eメール"});

    ColumnPositionMappingStrategy<User> strat = new ColumnPositionMappingStrategy<>();
    strat.setType(User.class);
    strat.setColumnMapping("username", "password", "firstName", "lastName", "email");

    StatefulBeanToCsv<User> beanToCsv = new StatefulBeanToCsvBuilder<User>(writer)
        .withMappingStrategy(strat)
        .build();
    beanToCsv.write(userList);
} catch (IOException e) {
    // 適宜例外処理
} catch (CsvException e) {
    // 適宜例外処理
}

CSVファイルへの書き出しは上記の通り。

Spring Boot 2.1でJUnit 5.5を使用する

testCompile 'org.springframework.boot:spring-boot-starter-test'

を以下の通り変更する。

testCompile('org.springframework.boot:spring-boot-starter-test') {
   exclude module: 'junit'
}

さらに、以下の行を追加する。

testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'

なお、Spring Boot 2.2からはJUnit 5.5が標準になったので、上記の対応は不要となる。

Coverallsをopenjdk11環境で使用する

ビルド環境をopenjdk11に変更したら、coveralls-maven-pluginが落ちてしまって、Coverallsのカバレッジが更新されなくなっていた。

coveralls-maven-pluginに以下の依存関係を追加することで回避することが出来る。

<dependencies>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>
</dependencies>

本家がJava11に対応してくれるのが一番良いのだが、忙しいみたいでいつ対応されるかわからない。

Thymeleafを試す

Thymeleafはテンプレートの一種。
Springとの相性が良く簡単に使用出来る。

・top.html

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <p th:text="Hello, Thymeleaf!">Hello, hide6644!</p>
</body>
</html>

View Resolverを設定する。

<!-- View Resolver for Thymeleaf -->
<bean id="templateResolver" class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
    <property name="prefix" value="/WEB-INF/pages/" />
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML" />
    <property name="cacheable" value="false" />
</bean>
 
<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver" />
    <property name="enableSpringELCompiler" value="true" />
</bean>
 
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
    <property name="templateEngine" ref="templateEngine" />
    <property name="characterEncoding" value="UTF-8" />
    <property name="order" value="4" />
</bean>

Veiwを呼び出す。

@RequestMapping(value = "top", method = RequestMethod.GET)
public String topRequest() {
    return "top";
}

YOMOU CRAWLERについて

小説を読もう!の更新を自動でチェックしたかったのだが、自分の好みに合ったサービスがなかった。
じゃあ、自分で作っちゃえってことで以下の通り。

  1. 主要機能
    • 小説の各話の登録更新状況をメールで通知する
    • 更新チェックではアクセス頻度を減らしサーバーに負荷を掛けないようにする
    • 小説が消されても、後で読めるように更新チェックと同時に、更新内容を保存する
  2. 稼働環境
    • 言語:Java11
    • データベース:MySQL
  3. 副題
    • Hibernateを学ぶ
    • オブジェクト指向を学ぶ
  4. 参考情報(小説を読もう!のページの構造)
    • 小説のトップページにはタイトル、作者名、概要、目次がある
    • 目次のリンクからは各話に遷移出来る
    • 各話にはタイトルと本文がある
  5. 詳細機能
    • htmlを取得しパースする
    • メール送信する