【问题标题】:Maintaining JNDI across multiple instances of Tomcat跨多个 Tomcat 实例维护 JNDI
【发布时间】:2010-11-10 18:30:32
【问题描述】:

我想知道人们如何管理跨 Tomcat 应用程序服务器的多个实例维护 JNDI 资源。让我们以我的数据库 JNDI 资源为例。它在我的 /conf/context.xml 文件中声明,并从我的应用程序 web.xml 文件中引用。

JNDI 资源必须在我的开发箱、暂存箱和生产箱上独立定义。如果我想设置开发人员/登台/生产框的新实例怎么办?这意味着我必须为我提出的每个新实例在 context.xml 中重新创建资源名称?从设置的角度来看,这里可能存在一些人为错误,可能导致应用服务器开始指向错误的数据库。

我发现这既麻烦又令人困惑,因为我增加了项目中的开发人员数量以及最终可能使用的生产服务器的数量。

每次重新安装 tomcat 并设置一个盒子时,您是否只是将其作为设置的一部分,或者创建一个设置脚本来处理这个问题?或者是否有一些其他级别的间接可以使这更容易?

【问题讨论】:

    标签: deployment tomcat jndi production-environment


    【解决方案1】:

    您是否正在部署多个必须使用共享资源的网络应用程序?

    如果没有,绝对没有理由在 /conf/context.xml 中声明您的资源。相反,它们应该在一个单独的、私有的 Web 应用程序 context.xml 文件中声明,该文件将在 WAR 中部署为 /META-INF/context.xml。该文件以及您的 web.xml 应检入您的源代码控制系统并作为构建的一部分进行部署 - 无需任何手动干预。

    如果您使用共享资源部署多个 Web 应用程序,您可以编写一个自定义资源工厂,将同一资源公开给多个 Web 应用程序(请参阅页面底部的 Tomcat documentation)并使用上述方法或 -至少对于开发环境 - 您可以在构建过程中自动更改(甚至默认替换)/conf/context.xml。当然,对于不推荐的生产部署。

    【讨论】:

    • 那些路过的投票真的很烦人。如果您要对某人投反对票,请鼓起勇气并发表评论为什么。当您认为我在回答中说错了话时,这会加倍 - 其他人会如何学习?
    • 我没有投反对票,但我强烈反对你。永远不应该在 WAR 本身中定义诸如 JDBC 数据源和邮件会话之类的资源。数据库连接参数是环境的属性,而不是应用程序的属性。应该能够在 WAR 投入生产之前将其部署在测试环境中。
    • 我同意莫里斯的观点,但我认为 ChssPly76 值得一票,因为他正在回答这个问题。 IIRC Bea Weblogic 有一个“JNDI 代理”,允许创建指向其他 JNDI 资源的 JNDI 引用
    • @Maurice - 关于资源是环境的属性,您是绝对正确的。但是,我不同意必须将 same WAR 部署在 dev box、qa 和生产上 - 例如,在 WAR 文件中包含/排除 context.xml 的决定可以由构建属性触发。最初的问题是“如何为多个开发人员自动化” - 在 context.xml 中执行此操作非常简单。您是否应该以相同的方式“自动化”多个生产/分段部署?当然不是。
    • same WAR 文件部署到 QA 和 PROD 可以确保在测试和上线之间没有任何变化。你不想要的是希望你没有忘记为生产构建检查正确版本的配置文件。这也意味着不必为从 prod 到 dev(补丁)到 test 到 prod 再到 dev(增强)再到 test 到 prod 等的每次转换提供 200 个版本的配置文件。
    【解决方案2】:

    JNDI 的重点是独立定义特定于环境的资源。您的开发、暂存和生产环境不应共享同一个数据库(或者无论如何,JNDI 旨在允许每个环境使用不同的数据库)。

    另一方面,如果您尝试对多个 Tomcat 服务器进行负载平衡,并且希望所有实例共享相同的配置,我认为您总是可以拆分您的 context.xml 并在一个共享文件。这是Tomcat documentation talking about context.xml

    如何管理这些取决于您。它可以很简单,例如每次创建新的 Tomcat 实例时都有一个“模板”context.xml 文件(将这些文件放在源代码控制系统中,尤其是分布式系统中会很有帮助)。或者您可以编写一个脚本来执行此操作。

    如果您想要更多,我相信有一些产品可以在整个过程中提供漂亮的用户界面。我相信这样做的人是SpringSource tc Server

    【讨论】:

    • 我不会在所有不同的服务器之间共享同一个数据库。您应该如何在各种服务器盒之间共享一个公共 context.xml?此外,如果确实似乎我必须为每个盒子维护各种 context.xml,那么有哪些好的流程/实践可以自动执行这些任务并减少人为错误?
    • 我认为如果不借助共享文件系统(例如,带有符号链接的 NFS),您将无法真正在不同的盒子之间共享文件。我还更新了答案以引用讨论 context.xml 配置的 Tomcat 文档。
    【解决方案3】:

    我假设对于给定的资源,您在每个环境中使用相同的 JNDI 名称。否则,您必须编辑代码以指向新资源 (JNDI) 名称。

    第一次设置环境几乎不可能提前测试。没有办法验证某些字符串(例如生产数据库连接字符串)在您实际必须使用它之前没有得到粗指。这是环境配置的本质。话虽如此,如果你想减少错误的可能性,首先你需要确保每个资源都有一个名称,无论它托管在哪个环境中都可以使用。在指向 jndi:/jdbc/myproject/resources/dbConnectionString 的属性文件中创建一个 dbConnectionString 资源名称,并确保所有环境都声明相同的资源。下面是我们如何使代码与这些类型的环境依赖项隔离开来。话虽如此,您将始终必须手动验证特定服务器的配置是否使用已定义资源的适当值。

    注意:从不创建资源名称,如“dbProdConnectionString”、“dbQAConnectionString”、“dbDevConnectionString”。您将违背尝试消除手动干预的目的,因为您添加了一个间接步骤,该步骤需要更改代码(将代码指向正确的资源名称)并构建(将更改打包到您的 .war 文件中) ) 在环境之间移动时。


    我们所做的是为特定于环境的属性创建一个文件夹结构。在该文件夹下,我们为每个针对部署的特定环境(包括本地开发)创建了文件夹。它看起来像这样:

    Project
    \
     -Properties
     \
      -Local (your PC)
      -Dev (shared dev server)
      -Test (pre-production)
      -Prod (Production)
    

    在每个文件夹中,我们放置属性/配置文件的并行副本,并将不同的配置仅放在相应文件夹中的文件中。秘诀是控制部署环境的类路径。我们在每个服务器上定义了一个 PROPERTIES 类路径条目。在 Prod 上,它将设置为“$ProjectDir/Properties/Prod”,而在 Test 上,相同的变量将设置为“$ProjectDir/Properties/Test”。

    通过这种方式,我们可以为 dev/test/prod 数据库预先配置数据库连接字符串,而不必在每次想要为不同的环境构建时都签出/在属性文件中。

    这也意味着我们可以将完全相同的 .war/.ear 文件部署到 Test 和 Prod 而无需重新构建。任何未在属性文件中声明的属性,我们以类似的方式处理,在每个环境中使用相同的 JNDI 名称,但使用特定于该环境的值。

    【讨论】:

      【解决方案4】:

      我的解决方案是将所有定义放入 server-template.xml 文件中,然后使用巧妙的 XSLT 转换为每个实例生成最终的 server.xml。我正在使用 ant 构建文件来复制 Tomcat 安装的所有文件,并让它从模板创建一个 server.xml。一切都保存在 CVS 中,因此我可以快速恢复安装,而不必担心某些内容可能无法正常工作。模板如下所示:

      <Server port="${tomcat.server.port}" shutdown="SHUTDOWN" 
        xmlns:x="http://my.company.com/tomcat-template">
      
        <x:define name="Derby-DataSource" username="???" password="???" url="???"
              auth="Container" type="javax.sql.DataSource"
              maxActive="50" maxIdle="5" maxWait="300"
              driverClassName="org.apache.derby.jdbc.ClientDriver"
              testWhileIdle="true" timeBetweenEvictionRunsMillis="3600000"
              removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" />
        <x:define x:element="Resource" name="Derby/Embedded/TDB" auth="Container" type="javax.sql.DataSource"
              maxActive="50" maxIdle="5" maxWait="300"
              username="" password="" driverClassName="org.apache.derby.jdbc.EmbeddedDriver"
              url="jdbc:derby:D:/tmp/TestDB" />
        <x:define x:element="Resource" name="Derby/TDB" auth="Container" type="javax.sql.DataSource"
              maxActive="50" maxIdle="5" maxWait="300"
              username="junit" password="test" driverClassName="org.apache.derby.jdbc.ClientDriver"
              url="jdbc:derby://localhost:1527/TDB" />
        <x:define x:element="Resource" name="Derby/P6/TDB" auth="Container" type="javax.sql.DataSource"
              maxActive="50" maxIdle="5" maxWait="300"
              username="junit" password="test" driverClassName="com.p6spy.engine.spy.P6SpyDriver"
              url="jdbc:derby://localhost:1527/TDB" />
      
         ... lots of Tomcat stuff ...
      
        <!-- Global JNDI resources -->
        <GlobalNamingResources>
          <x:if server="local">
            <!-- Local with Derby Network Server -->
            <x:use name="Derby/TDB"><x:override name="jdbc/DB" maxIdle="1" /></x:use>
            <x:use name="Derby/TDB"><x:override name="jdbc/DB_APP" maxIdle="1" /></x:use>
            <x:use name="Derby/TDB"><x:override name="jdbc/DB2" maxIdle="1" /></x:use>
          </x:if>
      
          <x:if env="test"> ... same for test </x:if>
          <x:if env="prod"> ... same for test </x:if>
        </GlobalNamingResources>
      </Server>
      

      如您所见,我定义了默认值,然后专门设置。在环境中,我会覆盖一些东西(本地系统获得的池比生产和集成测试更小)。

      过滤器脚本如下所示:

      <!-- 
      
      This XSLT Stylesheet transforms the file server-template.xml into server-new.xml.
      
      It will perform the following operations:
      
      - All x:define elements are removed
      - All x:use elements will be replaces with the content and attributes
        of the respective x:define element. The name of the new element is
        specified with the attribute "x:element".
      - All attributes in the x:override elements will overwrite the respective
        attributes from the x:define element.
      - x:if allows to suppress certain parts of the file altogether.
      
      Example:
      
        <x:define element="Resource" name="Derby/Embedded/TDB" auth="Container" ... />
        <x:use name="Derby/Embedded/TDB"><x:override name="NewTDB" /></x:use>
      
      becomes:
      
        <Resource name="NewTDB" auth="Container" ... />
      
      i.e. the attribute x:element="Resource" in x:define becomes the name of the
      new element, name="Derby/Embedded/ABSTDB" in x:use specifies which x:define
      to use and name="NewTDB" in x:override replaces the value of the "name"
      attribute in x:define.
      -->
      
      
      <xsl:stylesheet version="1.0" 
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
          xmlns:x="http://my.company.com/tomcat-template">
      <xsl:output method="xml"/>
      <!-- Key for fast lookup of x:defines -->
      <xsl:key name="def" match="//x:define" use="@name" />
      <!-- Parameters which can be used in x:if -->
      <xsl:param name="server" /> 
      <xsl:param name="env" />    
      <xsl:param name="instance" />   
      
      <!-- Copy everything by default -->
      <xsl:template match="node()|@*">
        <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>
      </xsl:template>
      
      <!-- filter x:defines -->
      <xsl:template match="x:define"></xsl:template>
      
      <!-- x:use is replaced by the matching x:define -->
      <xsl:template match="x:use">
        <!-- Find the x:define -->
        <xsl:variable name="def" select="key('def', @name)" />
        <!-- Save the x:use node in a variable -->
        <xsl:variable name="node" select="." />
      
        <!-- Start a new element. the name is in the attribute x:element of the x:define -->
        <xsl:element name="{$def/@x:element}">
          <!-- Process all attributes in the x: namespace -->
          <xsl:for-each select="$def/@x:*">
            <xsl:choose>
              <xsl:when test="name() = 'x:extends'">
                <xsl:variable name="extName" select="." />
                <xsl:variable name="ext" select="key('def', $extName)" />
                <xsl:for-each select="$ext/@*">
                  <xsl:if test="namespace-uri() != 'http://my.company.com/tomcat-template'">
                    <xsl:copy />
                  </xsl:if>
                </xsl:for-each>
              </xsl:when>
            </xsl:choose>
          </xsl:for-each>
      
          <!-- Copy all attributes from the x:define (except those in the x: namespace) -->
          <xsl:for-each select="$def/@*">
            <xsl:if test="namespace-uri() != 'http://my.company.com/tomcat-template'">
              <xsl:copy />
            </xsl:if>
          </xsl:for-each>
      
          <!-- If there is an x:override-Element, copy those attributes. This
               will overwrite existing attributes with the same name. -->
          <xsl:for-each select="$node/x:override/@*">
            <xsl:variable name="name" select="name()" />
            <xsl:variable name="value" select="." />
            <xsl:variable name="orig" select="$def/attribute::*[name() = $name]" />
      
            <xsl:choose>
              <!-- ${orig} allows to acces the attributes from the x:define -->
              <xsl:when test="contains($value, '${orig}')">
                <xsl:attribute name="{$name}"><xsl:value-of select="substring-before($value, '${orig}')" 
                  /><xsl:value-of select="$orig" 
                  /><xsl:value-of select="substring-after($value, '${orig}')" 
                  /></xsl:attribute>
              </xsl:when>
              <xsl:otherwise>
                <xsl:copy />
              </xsl:otherwise>
            </xsl:choose>
          </xsl:for-each>
          <!-- Copy all child nodes, too -->
          <xsl:apply-templates select="$def/node()"/>
        </xsl:element>
      </xsl:template>
      
      <!-- x:if, to filter parts of the document -->
      <xsl:template match="x:if">
        <!-- t will be non-empty if any of the conditions matches -->
        <xsl:variable name="t">
          <!-- Check for each paramater whether it is used in the x:if. If so,
               check the value. If the value is the same as the stylesheet
               paramater, the condition is met. Missing conditions count
               as met, too.
          <xsl:if test="not(@server) or @server = $server">1</xsl:if>
          <xsl:if test="not(@env) or @env = $env">1</xsl:if> 
          <xsl:if test="not(@instance) or @instance = $instance">1</xsl:if> 
        </xsl:variable>
        <xsl:if test="normalize-space($t) = '111'">
          <xsl:comment> <xsl:value-of select="$server" />, Env <xsl:value-of select="$env" />, Instance <xsl:value-of select="$instance" /> </xsl:comment>
          <xsl:apply-templates select="node()|@*"/>
        </xsl:if>
      </xsl:template>
      
      </xsl:stylesheet>
      

      【讨论】:

        【解决方案5】:

        如果您使用的是 spring 框架,您可以使用 PropertyPlaceholderConfigurer 轻松解决此问题。此类让您将定义移动到外部属性文件中。数据源配置如下:

        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName"><value>${jdbc.driver}</value></property>
        <property name="url"><value>${jdbc.url}</value></property>
        <property name="username"><value>${jdbc.user}</value></property>
        <property name="password"><value>${jdbc.password}</value></property>
        </bean>
        

        属性本身在标准属性文件中定义:

        jdbc.driverClassName=com.mysql.jdbc.Driver
        jdbc.url=jdbc:mysql://host/database
        jdbc.username=user
        jdbc.password=secret
        

        对于实际属性,您有两种选择:

        1. 将属性文件放在文件系统的外部,这样在每台机器上都会有所不同:

          <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="location">file:/etc/yourapp/jdbc.properties</property>
            <!-- on windows, put the file in c:\etc\yourapp, the definition will work -->
          </bean>
          
        2. 将以下系统属性添加到您的服务器 -Denv=[development|test|production]。然后,将三个配置文件放在您的 WEB-INF/classes 目录中:jdbc-development.properties、test-development.properties 和 production-development.properties。上下文配置将是:

          <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="location">classpath:jdbc-${env}.properties</property>
          </bean>
          

        【讨论】:

          【解决方案6】:

          如果您能够将远程 JNDI 目录绑定到 Tomcat 的“全局”JNDI 目录,则可以使用这种机制:Using a JNDI datasource created by another application with Tomcat

          问候。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-03-22
            • 2015-05-31
            • 1970-01-01
            • 2023-03-14
            • 1970-01-01
            • 2018-02-06
            • 1970-01-01
            • 2020-03-14
            相关资源
            最近更新 更多