はまる度に追記予定
spring-boot
spring-boot 1.3.3からmessages.propertiesが必須になった理由
messages-*.propertiesだけだとエラーになる
理由は以下に書いてある
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ファイルを参照するようにする場合は
# jarファイルへのシンボリックリンクへのシンボリックリンク
/etc/init.d/myapp -> /usr/local/myapp/myapp.jar
# 古い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の解決法
以下のようなコントローラーがあった場合
@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()
試行錯誤した結果、現時点での個人的にベターな方法が以下
@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})
または
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が入る
/**
* 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);
}
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() {
...
}
}
参考
- 対応されるかもしれないissue
追記
- spring-data-commonsが内部的に依存している
ApplicationEventPublisher
には適用されなかったorg.springframework.data.repository.core.support.EventPublishingRepositoryProxyPostProcessor
- そもそもApplicationEventPublisherをモックするよりはリスナー側をモックにして呼び出し確認する方が良いと思ったので方針を変更した