I've got a strange problem when using AspectJ, Neo4j NodeEntities and transactions.
I'm developing a spring application and using spring-data-neo4j with aspectj weaving. The problem is, that my fetched entities are empty outside of the transaction. Without AspectJ everything works as expected.
Here is a full test-case with maven:
pom.xml
<?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>org.test</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.4.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<neo4j.version>2.1.7</neo4j.version>
<spring-data-commons.version>1.10.0.RELEASE</spring-data-commons.version>
<spring-data-neo4j.version>3.3.0.RELEASE</spring-data-neo4j.version>
</properties>
<dependencies>
<!-- spring dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${spring-data-commons.version}</version>
</dependency>
<!-- spring + neo4j dependencies -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>${spring-data-neo4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-tx</artifactId>
<version>${spring-data-neo4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-aspects</artifactId>
<version>${spring-data-neo4j.version}</version>
</dependency>
<!-- for aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<!-- for testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-kernel</artifactId>
<version>${neo4j.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>aspects</id>
<build>
<plugins>
<!-- disable default compiler -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<!-- enable aspectj compiler -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.5</version>
</dependency>
</dependencies>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<outxml>true</outxml>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
<aspectLibrary>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
<complianceLevel>${java.version}</complianceLevel>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>spring-libs-milestone</id>
<name>Spring Milestone</name>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
</repositories>
</project>
demo/DemoNode.java
package demo;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.Indexed;
import org.springframework.data.neo4j.annotation.NodeEntity;
@NodeEntity
public class DemoNode {
@GraphId
private Long id;
@Indexed(unique = true)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
demo/DemoRepository.java
package demo;
import org.springframework.data.neo4j.repository.GraphRepository;
public interface DemoRepository extends GraphRepository<DemoNode> {
public DemoNode findByName(String name);
}
demo/DemoApplicationTests.java
package demo;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Transaction;
import org.neo4j.test.TestGraphDatabaseFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.aspects.config.Neo4jAspectConfiguration;
import org.springframework.data.neo4j.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.support.node.Neo4jHelper;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
public class DemoApplicationTests {
@Configuration
@ComponentScan("demo")
@EnableTransactionManagement
@EnableNeo4jRepositories(basePackages = "demo")
public static class DemoConfiguration extends Neo4jAspectConfiguration {
public DemoConfiguration() {
setBasePackage("demo");
}
@Bean(destroyMethod = "shutdown")
public GraphDatabaseService graphDatabaseService() {
return new TestGraphDatabaseFactory().newImpermanentDatabase();
}
}
@Autowired
private GraphDatabaseService graphDbService;
@Autowired
private DemoRepository demoRepo;
@Before
public void resetDb() {
Neo4jHelper.cleanDb(graphDbService);
}
// NOT successful
@Test
public void testNeo4jTransaction() {
DemoNode node;
final String name = "demo";
try (Transaction tx = graphDbService.beginTx()) {
node = new DemoNode();
node.setName(name);
node = demoRepo.save(node);
tx.success();
}
assertEquals(name, node.getName());
try (Transaction tx = graphDbService.beginTx()) {
node = demoRepo.findByName(name);
tx.success();
}
assertEquals(name, node.getName()); // expected:<demo> but was:<null>
}
// successful
@Test
public void testNeo4jWithGetTransaction() {
DemoNode node;
final String name = "demo";
try (Transaction tx = graphDbService.beginTx()) {
node = new DemoNode();
node.setName(name);
node = demoRepo.save(node);
tx.success();
}
assertEquals(name, node.getName());
try (Transaction tx = graphDbService.beginTx()) {
node = demoRepo.findByName(name);
tx.success();
}
try (Transaction tx = graphDbService.beginTx()) {
assertEquals(name, node.getName());
}
}
}
When you test this with mvn test
everything is fine. But when using aspectj (mvn test -Paspects
) it will fail:
DemoApplicationTests.testNeo4jNativeTransaction:64 expected:<demo> but was:<null>
What I found out when debugging this: Inside the transaction with AspectJ I could retrieve the name with the getter. BUT: All properties are always null
. So my guess is: AspectJ weaving stores the values somewhere else and overrides the setter/getter for this. The new getter requires an active transaction, so outside of the transaction it will not work/will use the default getter that doesn't work with the null
properties.
When I'm increasing the debug level I could see some message regarding this:
DEBUG o.s.d.n.f.DetachedEntityState - Outside of transaction, GET value from field class java.lang.String name rel: false idx: true
Am I doing something wrong? Do I need to create a clone and return this? Imho this would be an ugly way ...
Thanks in advance! I'm already searched and tried multiple hours but couldn't find any solution and any other problems here on stackoverflow or somewhere else in the internet. Maybe it's a bug ... maybe expected behaviour or maybe I'm doing some wrong ... I don't know.
UPDATE
I added a second test that works. It has a transaction around the getter. So when the getter is called in a transaction everything is fine. But I think this is a curious behaviour. Outside of the transaction the entity should be detached, but usable.