IK.AM

@making's tech note


Spring Data RESTのTips

🗃 {Programming/Java/org/springframework/data/rest}
🏷 Spring Data REST 🏷 Spring Boot 🏷 Java 
🗓 Updated at 2017-01-07T03:29:30Z  🗓 Created at 2017-01-06T08:40:42Z   🌎 English Page

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

また追加する。


✒️️ Edit  ⏰ History  🗑 Delete