Spring Data RESTのTips

Spring Data RESTはSpring DataのRepositoryを作ればREST APIとして公開してくれる便利プロジェクト。自分は好きで、RESTサービスを作る時は基本これを使う。

Spring Data RESTを使うと、RESTアプリケーションの作りは、典型的な

  • Controller -> Service -> Repository

ではなく

  • Repository -> RepositoryEventHandler

になる。

Tipsというか、いつも設定している内容をメモっておく。

サーバーサイド

リソースのURLにprefixをつける

デフォルトでは<Host Name>/<Resource Name(複数形)>になるが、Spring Data RESTが提供するAPIのパスにはprefixをつけた方が認可設定とかしやすい。

例えば、<Host Name>/v1/<Resource Name(複数形)>にする場合は、application.properties

spring.data.rest.base-path=/v1

を設定する。

Bean Validationを有効にする

依存関係の順番で@Lazyつけておかないとエラーになる。Validatorが複数登録されることもあるので、@Qualifier("mvcValidator")もあると無難。

	@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する設定を書く。

	@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)をつける。

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RestResource;

public interface FooBarRepository extends CrudRepository<FooBar, Long> {
	@RestResource(exported = false)
	FooBar save(FooBar foobar);

	@RestResource(exported = false)
	void delete(Long id);
}

欲しいAPIだけ定義する場合は空のRepositoryから定義していく。

import java.util.Optional;
import java.util.UUID;

import org.springframework.data.repository.Repository;

public interface FooBarRepository extends Repository<FooBar, Long> {
	Optional<FooBar> 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の処理が別トランザクションになってしまう。作り上難しいのだが、同じトランザクション内で処理するのに、無理やりインターセプタを適用する。

	// 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スコープが必要にしたい場合。

	@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に追加が必要。

	@Bean
	Jackson2ObjectMapperBuilderCustomizer objectMapperBuilderCustomizer() {
		return builder -> {
			builder.modulesToInstall(new Jackson2HalModule());
		};
	}

RestTemplateRestTemplateBuilderで作る。

	@Bean
	RestTemplate restTemplate(RestTemplateBuilder builder) {
		return builder.build();
	}

OAuth2RestTemplateを使う場合はMessageConverterを明示的に設定しないといけない。

	@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を依存ライブラリに追加すれば、OAuth2RestTemplateに自動でアクセストークンが入る。

security.oauth2.client.client-id=<Client ID>
security.oauth2.client.client-secret=<Client Secret>
security.oauth2.client.access-token-uri=<Auth Server URL>/oauth/token
security.oauth2.client.user-authorization-uri=<Auth Server URL>/oauth/authorize
security.oauth2.client.scope=account.read,account.write

また追加する。