LoginSignup
14
15

More than 5 years have passed since last update.

spring-boot + 関連プロダクト 細かいけどはまった箇所

Last updated at Posted at 2016-03-01

はまる度に追記予定

spring-boot

spring-boot 1.3.3からmessages.propertiesが必須になった理由

messages-*.propertiesだけだとエラーになる

理由は以下に書いてある

https://github.com/spring-projects/spring-boot/issues/4930

warプラグインを入れるとgradlew bootRepackageでjarファイルが生成されなくなる

providedCompileを使いたい等の理由でwarプラグインを入れた場合上記の問題が起こる

解決策

http://stackoverflow.com/questions/33432236/how-to-make-bootrepackage-depends-on-jar-not-war-when-using-gradle-war-plugin
https://github.com/spring-projects/spring-boot/issues/3931

追記

provided使いたいだけならpropdeps-pluginを入れたほうが良い
https://github.com/spring-projects/gradle-plugins/tree/master/propdeps-plugin

fully-executable jar での起動方法

導入は下記リンクでできるけど、

/usr/local/myapp以下の作業のみで完結させたい(/etc/init.d/以下をあまりいじりたくない)場合さらにシンボリックリンクを作ってバージョン名付きのjarファイルを参照するようにする場合は

/etc/init.d/myapp
# jarファイルへのシンボリックリンクへのシンボリックリンク
/etc/init.d/myapp -> /usr/local/myapp/myapp.jar
/usr/local/myapp
# 古いjarファイル
myapp-0.0.9.jar
# myapp.conf だと設定が読み込まれない
# "最終的に参照されるjarファイル名".conf にする必要がある
myapp-1.0.0.conf
myapp-1.0.0.jar
myapp.jar -> /usr/local/myapp/myapp-1.0.0.jar

起動スクリプト自体はjarファイルに埋め込まれているので

head -n 200 myapp.jar

とかで参照できる

メールのヘルスチェックが実行されてしまう

どのバージョンからからは自信なし(1.5.4.RELEASE以降かも)

原因

org.springframework.boot.actuate.autoconfigure.HealthIndicatorAutoConfiguration.MailHealthIndicatorConfiguration

を見れば分かるけど、

spring:
  mail:
    host: myhost
    port: 25

が適当に設定されていた為(実際はorg.springframework.mail.javamail.JavaMailSenderImplを使わずにアプリケーション固有の設定値を使って接続していた)

対処方法

spring.mail.*を適切に設定する

spring-bootのやり方に乗るのがベスト

設定を切る

spring.mail.*を除去

または

management:
  health:
    mail:
      enabled: false

spring-mvc + thymeleaf

Long型を@PathVariableに持つコントローラーのURLを${#mvc.url()}で解決できない

@Controller
@RequestMapping("/hoge")
public class HogeController {

  @RequestMapping(path = "{id}", method = RequestMethod.GET)
  public String detail(@PathVariable("id") Long id) {
    return "hoge/detail";
  }
}

みたいな(Long型を引数に持つ)コントローラーがある時に

<a th:href="${#mvc.url('HC#detail').arg(0, __${id}__).build()}">xxx</a>

でURLを解決できない。

解決策

<a th:href="${#mvc.url('HC#detail').arg(0, new Long(__${id}__)).build()}">xxx</a>

そもそもの話だけどクラス名のリファクタに弱いから#mvc.url()は使わないほうがいい(と後になって思った)

@ControllerAdviceを付けたクラス内にある@ModelAttributeを付けたメソッドが複数回呼ばれる

原因

@RestControllerへのアクセスも@ControllerAdviceで処理されるため

解決策

@ControllerAdvice("com.example.controller")
class GlobalMvcController {
}

@ControllerAdvice("com.example.api")
class GlobalRestController {
}

みたいにbasePackageをちゃんと指定する。
他にも適用範囲を指定できるオプションが結構あるので、特定の複数のコントローラーのみで共通化の処理を書くのに便利

コントローラー側でのURLの解決法

以下のようなコントローラーがあった場合

ProductController.java
@RequestMapping(path = "/products/{id}", method = RequestMethod.GET)
public String detail(@PathVariable("id") Long id, Model model) {
  model.addAttribute("product", service.findProduct(id));
  return "product";
}

上記パスへリダイレクトさせるURLを生成するコードはだいたいこんな感じ(他にもやり方はあるけど)

// パスの変更が不安
String.format("redirect:/products/%d", id);
// クラス・メソッド名の変更・後になって名前が重複するとかあるので危ない
"redirect:" + MvcUriComponentsBuilder.fromMappingName("PC#product").arg(0, id).build()

試行錯誤した結果、現時点での個人的にベターな方法が以下

ProductController.java
@RequestMapping(path = "/products/{id}", method = RequestMethod.GET)
public ModelAndView detail(@PathVariable("id") Long id) {
  ModelAndView mav = new ModelAndView("product"); 
  mav.addObject("product", service.findProduct(id));
  return mav;
}
// 長くてなんかイヤだけどMvcUriComponentsBuilderはstatic importすると短くなる
"redirect:" + MvcUriComponentsBuilder.fromMethodCall(MvcUriComponentsBuilder.on(ProductController.class).detail(id)).toUriString();

MvcUriComponentsBuilder#fromMethodCall()を使うことによる

  • 利点
    • URL、クラス名、メソッド名の変更に強い
    • 引数の変更に強い
  • 欠点
    • メソッドの戻り値にModelAndViewを使わざるをえない

spring-data-jpa / Hibernate

共通

like句に渡す文字列はエスケープする

@Repository
public interface ProductRepository extends JpaRepository<Product> {
  @Query("select p from Product p where p.name like '%?1%'")
  public List<Product> findProducts(String q);
}

@Service
public class ProductService {

  // ...

  public List<Product> ng() {
    return repository.findProducts("アルコール_40%");
  }

  public List<Product> ok() {
    return repository.findProducts("アルコール\_40\%");
  }
}

Date and Time APIを使う方法の変遷

Hibernate 5.1.x 以前

// org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters
@EntityScan(basePackageClasses = {Jsr310JpaConverters.class})

または

build.gradle
compile("org.hibernate:hibernate-java8")

Hibernate 5.2.x 以降

特に何もしなくてOK

静的クエリ(JPQL, HQL)

なんか知らないけどcross joinするSQL文が生成されてる

原因

select a from A a where a.deep.entity.reference.foo = ?1

みたいなJPQLクエリ。

解決策

冗長に感じるけど以下のように明示的にjoinすればcross joinされなくなる

select 
  a from A a 
  join a.deep d 
  join d.entity e 
  join e.reference r 
where 
  r.foo = ?1

動的クエリ(Criteria API, Specification)

同一Entityに対してFrom#join()を複数回呼ぶと生成されるSQL文でも複数回Joinが入る

UserSpecifications.java
/**
 * userを関連するdepartmentテーブルの内容で検索する
 */
public Specification<User> search(UserSearchCondition condition) {

  return (root, query, cb) -> {

    List<Predicate> predicates = new ArrayList<>();

    condition.departmentName()
      .map(dn -> cb.like(root.join(User_.department).get(Department_.name), '%' + dn + '%')
      .ifPresent(predicates::add); /* ここでdepartmentをjoin */

    condition.departmentAddress()
      .map(da -> cb.like(root.join(User_.department).get(Department_.address), '%' + da + '%')
      .ifPresent(predicates::add); /* ここでdepartmentをjoin (2回目) */

    // predicatesが空の時は本当は考慮しないといけないけど省略
    return cb.and(predicates.toArray(new Predicate[predicates.size()]));
  }

解決策

Fromからjoinを検索し、なければjoinして返却
※ クラスのフィールドにJoin<?, ?>をキャッシュさせる方法は使えない

Jpa Static MetaModelを使っている場合は以下のようなユーティリティメソッドを作る

public static <X, Y> Join<X, Y> getOrCreateJoin(From<?, X> from,
    SingularAttribute<? super X, Y> attribute, JoinType joinType) {

  for (Join<X, ?> join : from.getJoins()) {

    boolean sameName = join.getAttribute().equals(attribute);

    if (sameName && join.getJoinType().equals(joinType)) {
      return (Join<X, Y>) join;
    }
  }

  return from.join(attribute, joinType);
}

以下を参考
http://stackoverflow.com/questions/21791793/query-from-combined-spring-data-specification-has-multiple-joins-on-same-table

org.springframework.data.jpa.repository.query.QueryUtils

spring-data-jpaで@EmbeddableIdなEntityの保存で起こる問題

新規に永続化する際にEntityManager#persist()ではなくEntityManager#merge()がコールされる

原因

org.springframework.data.jpa.repository.support.SimpleJpaRepository#save(S)

org.springframework.data.repository.core.EntityInformation#isNew
falseで返るため

対応方法

DATAJPA-440 に記述があるが、
org.springframework.data.domain.Persistableを適切に実装する

が、以下のような実装にするとHibernate側の内部実装に起因するエラーでコケるので注意する(かなりハマった)

@Entity
public class MyEntity implements Persistable<MyEntityId> {

  @EmbeddedId
  private MyEntityId;

  @Override
  public boolean isNew() {
    return false;
  }
}

// 作成時のコード
void create() {

  // trueを返せばいいのは作成時だけなので
  // 匿名クラスでオーバーライドすればいいじゃんという考え
  MyEntity e = new MyEntity() {
    @Override public boolean isNew() {
      return true;
    }
  };

  // ...

  reoisutory.save(e);
}

どうやら@Transientを使ってisNew()の返り値を制御するのが正しいやり方らしい

@Entity
public class MyEntity implements Persistable<MyEntityId> {

  @Transient
  private boolean isNew = false;

  @EmbeddedId
  private MyEntityId;

  @Override
  public boolean isNew() {
    return isNew;
  }
}

// 作成時のコード
void create() {

  MyEntity e = new MyEntity();
  e.isNew = true;

  // ...

  reoisutory.save(e);
}

Flyway

本番環境(非Windows)で取得したダンプをローカル(Windows)にインポートするとその後のbootRunで落ちる

原因

プラットフォームでchecksumの算出結果が異なる為

解決策

3.2.1以前なら手動でchecksumを入れ直す

4.0.0で修正されるらしい
https://github.com/flyway/flyway/issues/253

Test

ApplicationEventPublisher@MockBeanできない

原因

(現在の)spring-frameworkの制約(らしい)

解決策

テスト時に以下のような設定を読み込む(つまり1.3.x時代と同じ)

@Configuration
public class MockEventPublisherConfig {

  @Bean
  @Primary
  public ApplicationEventPublisher eventPublisher() {
    return Mockito.mock(ApplicationEventPublisher.class);
  }
}
@SpringBootTest(classes = {MockEventPublisherConfig.class, ...})
public class MyTest {

  @test 
  public void test() {
    ...
  }
}

参考

追記

  • spring-data-commonsが内部的に依存しているApplicationEventPublisherには適用されなかった
    • org.springframework.data.repository.core.support.EventPublishingRepositoryProxyPostProcessor
  • そもそもApplicationEventPublisherをモックするよりはリスナー側をモックにして呼び出し確認する方が良いと思ったので方針を変更した
14
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
15