跨多个Tomcat实例维护JNDI

8
我想知道人们如何在多个Tomcat应用程序服务器实例之间管理维护JNDI资源。以我的数据库JNDI资源为例,它在/conf/context.xml文件中声明,并从我的应用程序的web.xml文件中引用。
这个JNDI资源必须在我的开发、暂存和生产环境中独立定义。如果我想设置一个新的开发/暂存/生产环境,那么我需要在每个新实例中重新创建context.xml中的资源名称吗?从设置的角度来看,这是可能会导致应用服务器开始指向错误数据库的地方,因此这很麻烦和令人困惑,尤其是当我的项目中的开发人员数量增加以及最终可能使用的生产服务器数量增加时。
您是否只是将其作为设置的一部分或创建一个设置脚本,每次重新安装Tomcat并设置一个新环境时都可以处理这个问题?或者是否有其他级别的间接方式可以使这更容易?
6个回答

3
我假设你在每个环境中都使用相同的JNDI名称来获取特定资源。否则,您将需要编辑代码以指向新的资源(JNDI)名称。
第一次设置环境几乎不可能预先测试。无法验证某些字符串(例如生产数据库连接字符串)是否输入错误,直到实际使用它为止。这就是环境配置的特性。话虽如此,如果您想减少出错的可能性,首先需要确保每个资源都被赋予一个名称,无论在哪个环境中托管,都使用该名称。在属性文件中创建dbConnectionString资源名称,指向jndi:/jdbc/myproject/resources/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”。
这样,我们可以预先配置开发/测试/生产数据库的连接字符串,而无需每次构建不同的环境时都要签出/签入属性文件。
这也意味着我们可以将完全相同的.war/.ear文件部署到测试和生产环境,而无需重新构建。任何未在属性文件中声明的属性都以类似的方式处理,即在每个环境中使用相同的JNDI名称,但使用特定于该环境的值。

1
如果你正在使用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>
      <!-- 在Windows上,将文件放在c:\etc\yourapp目录下,定义就会起作用-->
    </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>
    

1

您是否正在部署多个需要使用共享资源的Web应用程序?

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

如果您正在部署具有共享资源的多个Web应用程序,则可以编写自定义资源工厂,将同一资源公开给多个Web应用程序(请参见Tomcat文档页面底部),并使用上述方法或者至少在开发环境中自动更改(甚至替换默认情况下什么也不做的)/conf/context.xml作为构建的一部分。当然,对于生产部署来说,这是不可取的。


1
那些偷偷摸摸的负评真的很烦人。如果你要给某人投反对票,请勇敢地留下一条评论说明原因。尤其是当你认为我的回答有误时,更应该这样做——否则别人怎么会学习呢? - ChssPly76
5
我没有投反对票,但我强烈不同意您的观点。像JDBC数据源和邮件会话这样的资源不应该在WAR文件中定义。数据库连接参数是环境的属性,而不是应用程序的属性。在部署到生产环境之前,应该能够在测试环境中部署WAR文件。 - Maurice Perry
我同意Maurice的观点,但我认为ChssPly76值得得到一票赞成,因为他给出了问题的答案。如果我没记错的话,Bea Weblogic有一个“JNDI代理”,可以创建指向其他JNDI资源的JNDI引用。 - ATorras
@Maurice - 你的看法是完全正确的,资源确实是环境的属性。但是,我不同意必须在开发、qa和生产环境上部署相同的WAR文件 - 可以通过构建属性来触发包含/排除context.xml文件的决策,例如。最初的问题是“如何为多个开发人员自动化此过程” - 在context.xml中实现这一点非常简单。你是否应该以同样的方式“自动化”多个生产/预发布部署呢?当然不应该。 - ChssPly76
3
将相同的WAR文件部署到QA和PROD环境,可以确保在测试和上线之间没有任何变化。你不希望出现漏提交正确的生产构建配置文件版本的情况。这也意味着不需要为每个从prod到dev(补丁)、测试、prod到dev(增强)再到测试到prod的转换拥有200个配置文件版本。 - Kelly S. French
@Kelly - 我不想在本地配置文件中检查所有内容。部署到staging/production的构建来自具有其自己build.properties的CI box;本地开发构建使用统一设置,因此从源代码控制获取“所有”内容。我已经这样做很长时间了。我无法告诉你有多少次我不得不处理新开发人员报告的“我的构建失败”,因为他们没有在某个地方进行配置。是的,我们有一个详细的“新开发环境设置”wiki,但它没有帮助。用“checkout; ant all”替换它就可以了。 - ChssPly76

0
我的解决方案是将所有定义放入一个名为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>

0
JNDI 的目的是独立定义特定环境资源。您的开发、测试和生产环境不应该共享同一个数据库(或者说,JNDI 的设计就是允许每个环境使用单独的数据库)。
另一方面,如果您想要负载均衡多个 Tomcat 服务器,并且希望所有实例共享相同的配置,我认为您可以将 context.xml 拆分并在共享文件中设置共性部分。这里是 Tomcat 文档谈到 context.xml
如何管理这些由您决定。可以简单地创建一个“模板”context.xml 文件,每次创建新的 Tomcat 实例时都使用这个模板文件(尤其是在源代码控制系统中,特别是分布式系统中非常有用)。或者您可以编写脚本来完成这一操作。
如果您需要更多内容,我相信有一些产品可以在整个过程中提供良好的 UI 界面。其中一个我知道的是 SpringSource tc Server

我不会在各个服务器之间共享相同的数据库。您该如何在不同的服务器框中共享公共context.xml?此外,如果确实需要为每个框维护不同的context.xml,那么有哪些好的流程/惯例可以自动化这些任务并减少人为错误? - Ish
我不认为你能够在不使用共享文件系统(例如NFS带符号链接)的情况下在不同的盒子之间共享文件。我还更新了答案,引用了讨论context.xml配置的Tomcat文档。 - Jack Leow

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接