非托管服务器扩展

简介

如果您希望对应用程序与 Neo4j 的交互进行比 Cypher 提供的更细粒度的控制,则可以使用非托管服务器扩展。

这是一个锋利的工具,它允许用户将任意 JAX-RS 类部署到服务器,因此在使用时请谨慎。特别是,它可能导致服务器消耗大量堆空间并降低性能。如有疑问,请通过其中一个社区渠道寻求帮助。

编写非托管扩展的第一步是创建一个项目,其中包含对 Neo4j 核心 JAR 的依赖项。在 Maven 中,这可以通过将以下行添加到 POM 文件中来实现

<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j</artifactId>
    <version>5.25.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 在哪里查找扩展程序,方法是在neo4j.conf 中添加一些配置

#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 的事务性 Cypher HTTP 端点(请参阅 HTTP API 文档→事务性 Cypher 端点)将响应流式传输回客户端。例如,以下非托管扩展程序流式传输人员同事的数组

@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 3.5.15 开始,在更新 Jackson 依赖项后引入了重大更改。

Jackson v1 已不再受支持,并且积累了一些安全问题,例如

有关 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";
    }
}

完整的源代码位于: ColleaguesCypherExecutionResource.java

您的 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>5.25.1</version>
   <scope>test</scope>
</dependency>

测试工具包提供了一种机制来启动具有自定义配置和您选择的扩展程序的 Neo4j 实例。它还提供了一种机制来指定在启动 Neo4j 时包含的数据夹具,如以下示例所示

@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