非托管服务器扩展
介绍
如果希望对应用程序与 Neo4j 的交互拥有比 Cypher 更细粒度的控制,就需要使用非托管服务器扩展。
|
这是一把锋利的工具,允许用户将任意 JAX-RS 类部署到服务器上,使用时务必小心。尤其是,它可能会消耗大量服务器堆内存并降低性能。如有疑问,请通过社区渠道寻求帮助。 |
编写非托管扩展的第一步是创建一个项目,并在其中加入对 Neo4j 核心 JAR 的依赖。在 Maven 中,可通过在 POM 文件中添加以下行来实现。
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j</artifactId>
<version>2026.03.1</version>
<scope>provided</scope>
</dependency>
现在你已经准备好编写扩展了。
在代码中,你使用 DatabaseManagementService 与 Neo4j 交互,可以通过 @Context 注解获取它。下面的示例可作为编写扩展的模板。
@Path( "/helloworld" )
public class HelloWorldResource
{
private final DatabaseManagementService dbms;
public HelloWorldResource( @Context DatabaseManagementService dbms )
{
this.dbms = dbms;
}
@GET
@Produces( MediaType.TEXT_PLAIN )
@Path( "/{nodeId}" )
public Response hello( @PathParam( "nodeId" ) long nodeId )
{
// Do stuff with the database
return Response.status( Status.OK ).entity( UTF8.encode( "Hello World, nodeId=" + nodeId ) ).build();
}
}
完整源代码位于:HelloWorldResource.java
构建完成后,生成的 JAR 文件(以及任何自定义依赖)应放置在 $NEO4J_SERVER_HOME/plugins 目录下。还需要在 neo4j.conf 中添加配置,告诉 Neo4j 去哪里查找该扩展。
#Comma-separated list of JAXRS packages containing JAXRS Resource, one package name for each mountpoint. server.unmanaged_extension_classes=org.neo4j.examples.server.unmanaged=/examples/unmanaged
你的 hello 方法会在以下 URI 上响应 GET 请求:
http://{neo4j_server}:{neo4j_port}/examples/unmanaged/helloworld/{node_id}
例如:
curl https://:7474/examples/unmanaged/helloworld/123
其结果为:
Hello World, nodeId=123
流式 JSON 响应
编写非托管扩展时,你可以更细致地控制 Neo4j 查询使用的内存量。如果保持过多状态,会导致更频繁的完整垃圾回收,从而使 Neo4j 服务器响应变慢甚至失去响应。
状态容易增加的常见方式是创建 JSON 对象来表示查询结果,然后将其返回给客户端。Neo4j 的 HTTP 端点(参见 HTTP API 文档 → 运行事务)会把响应流式传输给客户端。例如,下面的非托管扩展会流式返回某个人的同事列表。
@Path("/colleagues")
public class ColleaguesResource
{
private DatabaseManagementService dbms;
private final ObjectMapper objectMapper;
private static final RelationshipType ACTED_IN = RelationshipType.withName( "ACTED_IN" );
private static final Label PERSON = Label.label( "Person" );
public ColleaguesResource( @Context DatabaseManagementService dbms )
{
this.dbms = dbms;
this.objectMapper = new ObjectMapper();
}
@GET
@Path("/{personName}")
public Response findColleagues( @PathParam("personName") final String personName )
{
StreamingOutput stream = new StreamingOutput()
{
@Override
public void write( OutputStream os ) throws IOException, WebApplicationException
{
JsonGenerator jg = objectMapper.getJsonFactory().createJsonGenerator( os, JsonEncoding.UTF8 );
jg.writeStartObject();
jg.writeFieldName( "colleagues" );
jg.writeStartArray();
final GraphDatabaseService graphDb = dbms.database( "neo4j" );
try ( Transaction tx = graphDb.beginTx();
ResourceIterator<Node> persons = tx.findNodes( PERSON, "name", personName ) )
{
while ( persons.hasNext() )
{
Node person = persons.next();
for ( Relationship actedIn : person.getRelationships( OUTGOING, ACTED_IN ) )
{
Node endNode = actedIn.getEndNode();
for ( Relationship colleagueActedIn : endNode.getRelationships( INCOMING, ACTED_IN ) )
{
Node colleague = colleagueActedIn.getStartNode();
if ( !colleague.equals( person ) )
{
jg.writeString( colleague.getProperty( "name" ).toString() );
}
}
}
}
tx.commit();
}
jg.writeEndArray();
jg.writeEndObject();
jg.flush();
jg.close();
}
};
return Response.ok().entity( stream ).type( MediaType.APPLICATION_JSON ).build();
}
}
完整源代码位于:ColleaguesResource.java
除了依赖 JAX-RS API,此示例还使用了 Jackson —— 一个 Java JSON 库。需要在 Maven POM(或等价文件)中加入以下依赖。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.2</version>
</dependency>
|
Neo4j 支持 Jackson v2。 关于 Jackson v2 的更多信息,请参阅 GitHub 上的 Jackson 项目。 |
你的 findColleagues 方法现在会在以下 URI 上响应 GET 请求:
http://{neo4j_server}:{neo4j_port}/examples/unmanaged/colleagues/{personName}
例如:
curl https://:7474/examples/unmanaged/colleagues/Keanu%20Reeves
其结果为:
{"colleagues":["Hugo Weaving","Carrie-Anne Moss","Laurence Fishburne"]}
执行 Cypher
可以通过注入的 GraphDatabaseService 来执行 Cypher 查询。例如,下面的非托管扩展使用 Cypher 检索某个人的同事。
@Path("/colleagues-cypher-execution")
public class ColleaguesCypherExecutionResource
{
private final ObjectMapper objectMapper;
private DatabaseManagementService dbms;
public ColleaguesCypherExecutionResource( @Context DatabaseManagementService dbms )
{
this.dbms = dbms;
this.objectMapper = new ObjectMapper();
}
@GET
@Path("/{personName}")
public Response findColleagues( @PathParam("personName") final String personName )
{
final Map<String, Object> params = MapUtil.map( "personName", personName );
StreamingOutput stream = new StreamingOutput()
{
@Override
public void write( OutputStream os ) throws IOException, WebApplicationException
{
JsonGenerator jg = objectMapper.getJsonFactory().createJsonGenerator( os, JsonEncoding.UTF8 );
jg.writeStartObject();
jg.writeFieldName( "colleagues" );
jg.writeStartArray();
final GraphDatabaseService graphDb = dbms.database( "neo4j" );
try ( Transaction tx = graphDb.beginTx();
Result result = tx.execute( colleaguesQuery(), params ) )
{
while ( result.hasNext() )
{
Map<String,Object> row = result.next();
jg.writeString( ((Node) row.get( "colleague" )).getProperty( "name" ).toString() );
}
tx.commit();
}
jg.writeEndArray();
jg.writeEndObject();
jg.flush();
jg.close();
}
};
return Response.ok().entity( stream ).type( MediaType.APPLICATION_JSON ).build();
}
private String colleaguesQuery()
{
return "MATCH (p:Person {name: $personName })-[:ACTED_IN]->()<-[:ACTED_IN]-(colleague) RETURN colleague";
}
}
你的 findColleagues 方法现在会在以下 URI 上响应 GET 请求:
http://{neo4j_server}:{neo4j_port}/examples/unmanaged/colleagues-cypher-execution/{personName}
例如:
curl https://:7474/examples/unmanaged/colleagues-cypher-execution/Keanu%20Reeves
其结果为:
{"colleagues": ["Hugo Weaving", "Carrie-Anne Moss", "Laurence Fishburne"]}
测试你的扩展
Neo4j 提供了一套工具,帮助你为扩展编写集成测试。通过在项目中添加以下测试依赖即可使用该工具包。
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>2026.03.1</version>
<scope>test</scope>
</dependency>
该测试工具包能够以自定义配置和所选扩展启动 Neo4j 实例,并提供机制指定在启动 Neo4j 时要加载的数据 Fixtures,下面的示例展示了这种用法。
@Path("")
public static class MyUnmanagedExtension
{
@GET
public Response myEndpoint()
{
return Response.ok().build();
}
}
@Test
public void testMyExtension() throws Exception
{
// Given
HTTP.Response response = HTTP.GET( HTTP.GET( neo4j.httpURI().resolve( "myExtension" ).toString() ).location() );
// Then
assertEquals( 200, response.status() );
}
@Test
public void testMyExtensionWithFunctionFixture()
{
final GraphDatabaseService graphDatabaseService = neo4j.defaultDatabaseService();
try ( Transaction transaction = graphDatabaseService.beginTx() )
{
// Given
Result result = transaction.execute( "MATCH (n:User) return n" );
// Then
assertEquals( 1, count( result ) );
transaction.commit();
}
}
示例的完整源代码位于:ExtensionTestingDocIT.java
请注意使用 server.httpURI().resolve( "myExtension" ) 来确保使用正确的基础 URI。
如果你使用 JUnit 测试框架,亦可使用相应的 JUnit 规则。
@Rule
public Neo4jRule neo4j = new Neo4jRule()
.withFixture( "CREATE (admin:Admin)" )
.withFixture( graphDatabaseService ->
{
try (Transaction tx = graphDatabaseService.beginTx())
{
tx.createNode( Label.label( "Admin" ) );
tx.commit();
}
return null;
} );
@Test
public void shouldWorkWithServer()
{
// Given
URI serverURI = neo4j.httpURI();
// When you access the server
HTTP.Response response = HTTP.GET( serverURI.toString() );
// Then it should reply
assertEquals(200, response.status());
// and you have access to underlying GraphDatabaseService
try (Transaction tx = neo4j.defaultDatabaseService().beginTx()) {
assertEquals( 2, count(tx.findNodes( Label.label( "Admin" ) ) ));
tx.commit();
}
}
示例的完整源代码位于:JUnitDocIT.java