Play Framework 无法读取 Cloud Foundry 中的环境变量

Play Framework unable to read environment variable in Cloud Foundry

我正在尝试读取 application.conf 中 PostgreSQL 服务的连接 URL 环境变量,如下所示:

db.default.driver="org.postgresql.Driver"
db.default.url=${?cloud.services.postgresql.connection.url}

我的VCAP_SERVICES如下

{
  "postgresql": [
    {
      "binding_name": null,
      "credentials": {
        "dbname": "sample-db",
        "end_points": [
          {
            "host": "x.x.x.x",
            "network_id": "SF",
            "port": "44980"
          }
        ],
        "hostname": "x.x.x.x",
        "password": "sample-password",
        "port": "44980",
        "ports": {
          "5432/tcp": "44980"
        },
        "uri": "postgres://sample-user:sample-password@x.x.x.x:44980/sample-db",
        "username": "sample-user"
      },
      "instance_name": "postgresql",
      "label": "postgresql",
      "name": "postgresql",
      "plan": "v9.6-dev",
      "provider": null,
      "syslog_drain_url": null,
      "tags": [
        "postgresql",
        "relational"
      ],
      "volume_mounts": []
    }
  ]
}

我正在关注 this article

但是数据库不会配置并且根是 Configuration error[jdbcUrl is required with driverClassName.]。下面是完整的异常转储。

play.api.Configuration$$anon: Configuration error[Cannot initialize to database [default]]
    at play.api.Configuration$.configError(Configuration.scala:155)
    at play.api.Configuration.reportError(Configuration.scala:394)
    at play.api.db.DefaultDBApi.$anonfun$initialize(DefaultDBApi.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:333)
    at play.api.db.DefaultDBApi.initialize(DefaultDBApi.scala:68)
    at play.api.db.DBApiProvider.get$lzycompute(DBModule.scala:92)
    at play.api.db.DBApiProvider.get(DBModule.scala:77)
    at play.api.db.DBApiProvider.get(DBModule.scala:59)
    at com.google.inject.internal.ProviderInternalFactory.provision(ProviderInternalFactory.java:85)
    at com.google.inject.internal.BoundProviderFactory.provision(BoundProviderFactory.java:77)
    at com.google.inject.internal.ProviderInternalFactory.circularGet(ProviderInternalFactory.java:59)
    at com.google.inject.internal.BoundProviderFactory.get(BoundProviderFactory.java:61)
    at com.google.inject.internal.SingleFieldInjector.inject(SingleFieldInjector.java:52)
    at com.google.inject.internal.MembersInjectorImpl.injectMembers(MembersInjectorImpl.java:147)
    at com.google.inject.internal.MembersInjectorImpl.injectAndNotify(MembersInjectorImpl.java:101)
    at com.google.inject.internal.MembersInjectorImpl.injectMembers(MembersInjectorImpl.java:71)
    at com.google.inject.internal.InjectorImpl.injectMembers(InjectorImpl.java:1055)
    at com.google.inject.util.Providers$GuicifiedProviderWithDependencies.initialize(Providers.java:154)
    at com.google.inject.util.Providers$GuicifiedProviderWithDependencies$$FastClassByGuice$a7177aa.invoke(<generated>)
    at com.google.inject.internal.SingleMethodInjector.invoke(SingleMethodInjector.java:51)
    at com.google.inject.internal.SingleMethodInjector.inject(SingleMethodInjector.java:85)
    at com.google.inject.internal.MembersInjectorImpl.injectMembers(MembersInjectorImpl.java:147)
    at com.google.inject.internal.MembersInjectorImpl.injectAndNotify(MembersInjectorImpl.java:101)
    at com.google.inject.internal.Initializer$InjectableReference.get(Initializer.java:245)
    at com.google.inject.internal.Initializer.injectAll(Initializer.java:140)
    at com.google.inject.internal.InternalInjectorCreator.injectDynamically(InternalInjectorCreator.java:178)
    at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:111)
    at com.google.inject.Guice.createInjector(Guice.java:87)
    at com.google.inject.Guice.createInjector(Guice.java:78)
    at play.api.inject.guice.GuiceBuilder.injector(GuiceInjectorBuilder.scala:200)
    at play.inject.guice.GuiceBuilder.injector(GuiceBuilder.java:211)
    at play.inject.guice.GuiceApplicationBuilder.build(GuiceApplicationBuilder.java:121)
    at play.inject.guice.GuiceApplicationLoader.load(GuiceApplicationLoader.java:32)
    at play.api.ApplicationLoader$JavaApplicationLoaderAdapter.load(ApplicationLoader.scala:181)
    at play.core.server.DevServerStart$$anon.$anonfun$reload(DevServerStart.scala:190)
    at play.utils.Threads$.withContextClassLoader(Threads.scala:22)
    at play.core.server.DevServerStart$$anon.reload(DevServerStart.scala:182)
    at play.core.server.DevServerStart$$anon.get(DevServerStart.scala:142)
    at play.core.server.AkkaHttpServer.handleRequest(AkkaHttpServer.scala:301)
    at play.core.server.AkkaHttpServer.$anonfun$createServerBinding(AkkaHttpServer.scala:191)
    at akka.stream.impl.fusing.MapAsync$$anon.onPush(Ops.scala:1285)
    at akka.stream.impl.fusing.GraphInterpreter.processPush(GraphInterpreter.scala:541)
    at akka.stream.impl.fusing.GraphInterpreter.execute(GraphInterpreter.scala:423)
    at akka.stream.impl.fusing.GraphInterpreterShell.runBatch(ActorGraphInterpreter.scala:625)
    at akka.stream.impl.fusing.GraphInterpreterShell$AsyncInput.execute(ActorGraphInterpreter.scala:502)
    at akka.stream.impl.fusing.GraphInterpreterShell.processEvent(ActorGraphInterpreter.scala:600)
    at akka.stream.impl.fusing.ActorGraphInterpreter.akka$stream$impl$fusing$ActorGraphInterpreter$$processEvent(ActorGraphInterpreter.scala:769)
    at akka.stream.impl.fusing.ActorGraphInterpreter$$anonfun$receive.applyOrElse(ActorGraphInterpreter.scala:784)
    at akka.actor.Actor.aroundReceive(Actor.scala:535)
    at akka.actor.Actor.aroundReceive$(Actor.scala:533)
    at akka.stream.impl.fusing.ActorGraphInterpreter.aroundReceive(ActorGraphInterpreter.scala:691)
    at akka.actor.ActorCell.receiveMessage(ActorCell.scala:575)
    at akka.actor.ActorCell.invoke(ActorCell.scala:545)
    at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:270)
    at akka.dispatch.Mailbox.run(Mailbox.scala:231)
    at akka.dispatch.Mailbox.exec(Mailbox.scala:243)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: play.api.Configuration$$anon: Configuration error[jdbcUrl is required with driverClassName.]
    at play.api.Configuration$.configError(Configuration.scala:155)
    at play.api.Configuration.reportError(Configuration.scala:394)
    at play.api.db.HikariCPConnectionPool.create(HikariCPModule.scala:70)
    at play.api.db.PooledDatabase.createDataSource(Databases.scala:249)
    at play.api.db.DefaultDatabase.dataSource$lzycompute(Databases.scala:141)
    at play.api.db.DefaultDatabase.dataSource(Databases.scala:139)
    at play.api.db.DefaultDBApi.$anonfun$initialize(DefaultDBApi.scala:72)
    ... 57 common frames omitted
Caused by: java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.
    at com.zaxxer.hikari.HikariConfig.validate(HikariConfig.java:1000)
    at play.api.db.HikariCPConfig.toHikariConfig(HikariCPModule.scala:140)
    at play.api.db.HikariCPConnectionPool.$anonfun$create(HikariCPModule.scala:57)
    at scala.util.Try$.apply(Try.scala:210)
    at play.api.db.HikariCPConnectionPool.create(HikariCPModule.scala:54)
    ... 61 common frames omitted

使用 Play Framework 2.8。

看起来 Cloud Foundry 对 Play Framework 的支持自 Play 2.5 以来已经中断。 Play Framework 中 Cloud Foundry 的 Java Buildpack removed support for it which in turn invalidates the corresponding documentation 指的是前缀为 {?cloud.services....}.

的配置键

我自己编写代码来解析 VCAP_SERVICES 并将其插入应用程序加载器,following the documentation to create your own application loader:

package com.example.config;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

import play.ApplicationLoader;
import play.inject.guice.GuiceApplicationBuilder;
import play.inject.guice.GuiceApplicationLoader;


public class MyApplicationLoader extends GuiceApplicationLoader  {
    private static final Logger LOGGER = Logger.getLogger(MyApplicationLoader.class.getCanonicalName());

    @Override
    public GuiceApplicationBuilder builder(ApplicationLoader.Context context) {
        // https://www.programcreek.com/scala/play.api.Configuration
        // https://www.playframework.com/documentation/2.8.x/JavaDependencyInjection#Advanced:-Extending-the-GuiceApplicationLoader
        
        Config cloudConfig = parseCloudFoundryEnvironmentConfig(context);

        return initialBuilder
                    .in(context.environment())
                    .loadConfig(cloudConfig.withFallback(context.initialConfig()))
                    .overrides(overrides(context));
    }

    static Config parseCloudFoundryEnvironmentConfig(ApplicationLoader.Context context) {
        final ObjectMapper objectMapper = new ObjectMapper();
        final HashMap<String,Object> configOutput = new HashMap<>();
        
        final String vcap_services_str = System.getenv("VCAP_SERVICES");
        if(vcap_services_str != null) {
            try {
                JsonNode rootNode = objectMapper.readTree(vcap_services_str);
                /// ... parse VCAP_SERVICES and initialize the "db...." Play configuration into `configOutput`
            } catch(IOException ex) {
                LOGGER.log(Level.SEVERE, ex, () -> MessageFormat.format("Unable to parse VCAP_SERVICES content: {0}", vcap_services_str));
            }           
        } else {
            LOGGER.info("VCAP_SERVICES not defined");
        }
        Config configResult = ConfigFactory.parseMap(configOutput, "Environment Variables");
        return configResult;
    }
}

然后我通过在 conf 文件夹下的 reference.conf 配置文件中创建一个条目来激活这个新的应用程序加载器。

play.application.loader=com.example.config.MyApplicationLoader