--- title: Spring Data RESTのTips tags: ["Spring Data REST", "Spring Boot", "Java"] categories: ["Programming", "Java", "org", "springframework", "data", "rest"] date: 2017-01-06T08:40:42Z updated: 2017-01-07T03:29:30Z --- [Spring Data REST](http://projects.spring.io/spring-data-rest/)はSpring Dataの`Repository`を作ればREST APIとして公開してくれる便利プロジェクト。自分は好きで、RESTサービスを作る時は基本これを使う。 Spring Data RESTを使うと、RESTアプリケーションの作りは、典型的な * Controller -> Service -> Repository ではなく * Repository -> RepositoryEventHandler になる。 Tipsというか、いつも設定している内容をメモっておく。 ### サーバーサイド #### リソースのURLにprefixをつける デフォルトでは`/`になるが、Spring Data RESTが提供するAPIのパスにはprefixをつけた方が認可設定とかしやすい。 例えば、`/v1/`にする場合は、`application.properties`に ``` properties spring.data.rest.base-path=/v1 ``` を設定する。 #### Bean Validationを有効にする 依存関係の順番で`@Lazy`つけておかないとエラーになる。`Validator`が複数登録されることもあるので、`@Qualifier("mvcValidator")`もあると無難。 ``` java @Configuration public static class RestConfig extends RepositoryRestConfigurerAdapter { private final Validator validator; public RestConfig(@Lazy @Qualifier("mvcValidator") Validator validator) { this.validator = validator; } @Override public void configureValidatingRepositoryEventListener( ValidatingRepositoryEventListener validatingListener) { validatingListener.addValidator("beforeCreate", validator); validatingListener.addValidator("beforeSave", validator); } /* ... */ } ``` #### EntityのIDをレスポンスに含める デフォルトだと、リソースのレスポンスにIDフィールドが含まれず、`_links`にエンティにアクセスするためのURLが入る。 これでもいいが、ID入っている方が便利な時があるので、その時は明示的にexposeする設定を書く。 ``` java @Configuration public static class RestConfig extends RepositoryRestConfigurerAdapter { /* ... */ @Override public void configureRepositoryRestConfiguration( RepositoryRestConfiguration config) { config.exposeIdsFor(FooBar.class); } } ``` ### 一部のAPIだけRESTで公開する デフォルトだと`Repository`のCRUD全APIが公開される。 公開して欲しくないAPIを除く場合はメソッドをOverrideして`@RestResource(exported = false)`をつける。 ``` java import org.springframework.data.repository.CrudRepository; import org.springframework.data.rest.core.annotation.RestResource; public interface FooBarRepository extends CrudRepository { @RestResource(exported = false) FooBar save(FooBar foobar); @RestResource(exported = false) void delete(Long id); } ``` 欲しいAPIだけ定義する場合は空の`Repository`から定義していく。 ``` java import java.util.Optional; import java.util.UUID; import org.springframework.data.repository.Repository; public interface FooBarRepository extends Repository { Optional findOne(Long id); FooBar save(FooBar foobar); void delete(Long id); } ``` 公開されているAPIは`http://localhost:8080/profile/foobars`で確認できる(`spring.data.rest.base-path=/v1`が付いている場合は`http://localhost:8080/v1/profile/foobars`)。 #### トランザクション管理 デフォルトでは`@RepositoryEventHandler`の処理と`Repository`の処理が別トランザクションになってしまう。作り上難しいのだが、同じトランザクション内で処理するのに、無理やりインターセプタを適用する。 ``` java // Workarround http://stackoverflow.com/a/30713264/5861829 @Configuration static class TxConfig { private final PlatformTransactionManager transactionManager; public TxConfig(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } @Bean TransactionInterceptor txAdvice() { NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource(); source.addTransactionalMethod("post*", new DefaultTransactionAttribute()); source.addTransactionalMethod("put*", new DefaultTransactionAttribute()); source.addTransactionalMethod("delete*", new DefaultTransactionAttribute()); source.addTransactionalMethod("patch*", new DefaultTransactionAttribute()); return new TransactionInterceptor(transactionManager, source); } @Bean Advisor txAdvisor() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression( "execution(* org.springframework.data.rest.webmvc.RepositoryEntityController.*(..))"); return new DefaultPointcutAdvisor(pointcut, txAdvice()); } } ``` #### OAuth2のリソースサーバーにした時のスコープによる認可設定 例えばAccountリソース(パスは`/v1/accoounts`)に対してGETは`account.read`または`admin.read`スコープ、POST, PUT, PATCH, DELETEには`account.write`または`admin.write`スコープが必要にしたい場合。 ``` java @Configuration @EnableResourceServer static class OAuth2ResourceConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .antMatcher("/v1/**").authorizeRequests() .antMatchers(HttpMethod.GET, "/v1/accounts/**") .access("#oauth2.hasAnyScope('account.read', 'admin.read')") .antMatchers(HttpMethod.POST, "/v1/accounts/**") .access("#oauth2.hasAnyScope('account.write', 'admin.write')") .antMatchers(HttpMethod.PUT, "/v1/accounts/**") .access("#oauth2.hasAnyScope('account.write', 'admin.write')") .antMatchers(HttpMethod.PATCH, "/v1/accounts/**") .access("#oauth2.hasAnyScope('account.write', 'admin.write')") .antMatchers(HttpMethod.DELETE, "/v1/accounts/**") .access("#oauth2.hasAnyScope('account.write', 'admin.write')"); } } ``` 当然、OAuth2クライアントに対するスコープの設定は認可サーバー側の話。 ### クライアントサイド #### `Jackson2HalModule`を有効にする サーバーサイドが`spring-data-rest`だと`Resources`型ででシリアライズしたい。デフォルトではでシリアライザが有効にならないので`ObjectMapper`に追加が必要。 ``` java @Bean Jackson2ObjectMapperBuilderCustomizer objectMapperBuilderCustomizer() { return builder -> { builder.modulesToInstall(new Jackson2HalModule()); }; } ``` `RestTemplate`は`RestTemplateBuilder`で作る。 ``` java @Bean RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } ``` `OAuth2RestTemplate`を使う場合は`MessageConverter`を明示的に設定しないといけない。 ``` java @Bean OAuth2RestTemplate restTemplate(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails resource, HttpMessageConverters messageConverters, ObjectMapper objectMapper) { OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resource, oauth2ClientContext); // Set same message converters as RestTemplate to use Jackson2HalModule restTemplate.setMessageConverters(messageConverters.getConverters()); // ... return restTemplate; } ``` ちなみにwebアプリ側で`@EnableOAuthSso`を使っている場合は、でトークン上記の認可設定をしている場合は、次の設定をして[`spring-cloud-starter-oauth2`](https://github.com/spring-cloud/spring-cloud-security)を依存ライブラリに追加すれば、`OAuth2RestTemplate`に自動でアクセストークンが入る。 ``` properties security.oauth2.client.client-id= security.oauth2.client.client-secret= security.oauth2.client.access-token-uri=/oauth/token security.oauth2.client.user-authorization-uri=/oauth/authorize security.oauth2.client.scope=account.read,account.write ``` --- また追加する。