I’m using Spring Data Rest to expose rest endpoints which i can use in my user interface. However, during testing I noticed that when hitting the base rest url (http://localhost:8080/rest) , the endpoints are inconsistently exposed. I’m using the Annotation based RepositoryDetectionStrategies.
I would appreciate some assistance in understanding and resolving the issue.
Example:
First boot: all endpoints are properly exposed:
{ "_links" : { "orders" : { "href" : "http://localhost:8080/rest/orders{?page,size,sort}", "templated" : true }, "reports" : { "href" : "http://localhost:8080/rest/reports{?page,size,sort}", "templated" : true }, "buySells" : { "href" : "http://localhost:8080/rest/positions{?page,size,sort}", "templated" : true }, "profile" : { "href" : "http://localhost:8080/rest/profile" } } }
After a restart without any code or configuration changes:
{ "_links" : { "orders" : { "href" : "http://localhost:8080/rest/orders{?page,size,sort}", "templated" : true }, "buySells" : { "href" : "http://localhost:8080/rest/positions{?page,size,sort}", "templated" : true }, "profile" : { "href" : "http://localhost:8080/rest/profile" } } }
As you can see, the reports endpoint is not exposed after the restart of the application. The exposure failure is very inconsistent; sometimes all 3 endpoints are not exposed, sometimes 1 or 2 are missing (inconsistent which) and sometimes everything is fine.
I’m not able to find any failures in the logs, even when putting it to TRACE.
Configuration
Here is the main pom:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.rvh</groupId> <artifactId>project-parent</artifactId> <version>0.0.1-SNAPSHOT</version> <name>project-parent</name> <packaging>pom</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <slf4j.version>1.7.30</slf4j.version> </properties> <modules> <!-- <module>compiler-plugin-java-9</module> --> <!-- We haven't upgraded to java 9. --> <module>web</module> <module>api-engine</module> </modules> <dependencyManagement> <dependencies> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version> </dependency> <dependency> <groupId>nl.rvh</groupId> <artifactId>business-rule-validator</artifactId> <version>1.0</version> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-maven-plugin</artifactId>--> <!-- <version>2.5.3</version>--> <!-- </dependency>--> </dependencies> </dependencyManagement> <dependencies> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-devtools</artifactId>--> <!-- <optional>true</optional>--> <!-- </dependency>--> </dependencies> <build> <finalName>collector</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <configuration> <mainClass>com.rvh.collector.CollectorApplication</mainClass> </configuration> </execution> </executions> </plugin> <!-- mvn sonar:sonar --> <!-- -Dsonar.projectKey=baralga --> <!-- -Dsonar.organization=baralga --> <!-- -Dsonar.host.url=https://sonarcloud.io --> <!-- -Dsonar.login=<GENERATED_TOKEN>--> <plugin> <groupId>org.sonarsource.scanner.maven</groupId> <artifactId>sonar-maven-plugin</artifactId> <version>3.7.0.1746</version> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.6</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>prepare-package</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <useSystemClassLoader>false</useSystemClassLoader> <argLine>-Dfile.encoding=UTF8</argLine> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> </plugins> </build> </project>
here is the web module pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.rvh</groupId> <artifactId>project-parent</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>collector</artifactId> <version>0.0.1-SNAPSHOT</version> <name>collector</name> <description>collector application</description> <packaging>war</packaging> <properties> <main.basedir>${basedir}/../..</main.basedir> <m2eclipse.wtp.contextRoot>/</m2eclipse.wtp.contextRoot> <java.version>11</java.version> <sonar.coverage.jacoco.xmlReportPaths>${project.build.directory}/site/jacoco/jacoco.xml </sonar.coverage.jacoco.xmlReportPaths> </properties> <dependencies> <!-- Compile --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-data</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-devtools</artifactId>--> <!-- <optional>true</optional>--> <!-- </dependency>--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-media</artifactId> <version>17-ea+14</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency> <dependency> <groupId>com.rvh.api.engine</groupId> <artifactId>apiEngine</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!-- Provided --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>nl.rvh.trade</groupId> <artifactId>kraken-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>nl.rvh</groupId> <artifactId>business-rule-validator</artifactId> </dependency> <!-- Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
This is how the main class is annotated:
@Configuration @ComponentScan("com.rvh.**") @EnableAutoConfiguration @EnableJpaRepositories("com.rvh.collector") @EnableScheduling public class CollectorApplication {
this is the rest config class:
@Configuration public class RestRepositoryConfig implements RepositoryRestConfigurer { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { //Only expose annotated repositories config.setRepositoryDetectionStrategy(RepositoryDetectionStrategy.RepositoryDetectionStrategies.ANNOTATED); config.setBasePath("/rest"); ExposureConfiguration exposureConfiguration = config.getExposureConfiguration(); exposureConfiguration.withItemExposure((metadata, httpMethods) -> httpMethods .disable(HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.TRACE, HttpMethod.HEAD, HttpMethod.PUT, HttpMethod.POST)); } }
and my rest repo’s, which are all part of the com.rvh.collector.rest package.
@RepositoryRestResource @PreAuthorize("hasRole('ROLE_ADMIN')") public interface OrdersRest extends PagingAndSortingRepository<Order, Integer> { } @RepositoryRestResource(path = "positions") @PreAuthorize("hasRole('ROLE_ADMIN')") public interface PositionRestRepo extends PagingAndSortingRepository<BuySell, Integer> { } @RepositoryRestResource @PreAuthorize("hasRole('ROLE_ADMIN')") public interface ReportsDao extends PagingAndSortingRepository<Report, Long> { @Query("select report from Report report where report.accountId in " + "(select acc.id from User user join user.accounts acc where user.userName = ?#{ principal?.username }) ") Iterable<Report> findAll(); @Query("select report from Report report where report.accountId in " + "(select acc.id from User user join user.accounts acc where user.userName = ?#{ principal?.username })" + "and report.id = :aLong ") Optional<Report> findById(Long aLong); }
Advertisement
Answer
I believe I found the issue;
I was having multiple repositories for the same entity. For example: I had an OrdersDao and a OrderRestRepo for the Order entity which where both an repository interface. The reason for creating 2 repo’s for the same entity was because I want the OrderRestRepo to utilize @preauthorize functionality, which I did not want to apply to the OrdersDao as that’s also used by one my of schedulers.
When moving to a single repo, single entity I have not seen the inconsistent exposure of the endpoints (rebooted about 6 / 7 times consistently).
For the scheduler issue I’ll probably reside to the solution #2 described in the following so: SecurityContext with default System authentication/user
Update: i found some defect on Spring Github which explains the exact behavior I noticed: https://github.com/spring-projects/spring-data-rest/issues/1286
It seems to be a known issue. The suggested solution is to use @Primary annotation on the exposed repositry and set the non-exposed reposity to Exported = False. From the defect listed it seems that @Primary initially did not work as expected, but this may have been fixed over time. The defect is still marked as open so i’m just gonna try the suggested and otherwise reside to the initial solution.
Update 2
So far using the @Primary to the repository which should be exposed as Rest, is working as expected. So I now have 2 repo’s for the same entity.
@Repository @Qualifier(value = "ordersDao") public interface OrdersDao extends JpaRepository<Order, Integer>, JpaSpecificationExecutor<Order> {
And
@RepositoryRestResource @PreAuthorize("hasRole('ROLE_ADMIN')") @Primary public interface OrdersRest extends PagingAndSortingRepository<Order, Integer> {
As i’m using the annotation based strategy I don’t have to set the Exporter=false attribute to the OrdersDao. (That Dao is not using the @(Repository)RestResource annotation either, else it would be required).
I’ll accept this answer once i’m able to