How Tomcat Works(十八)

在前面的文章中,如果我们要启动tomcat容器,我们需要使用Bootstrap类来实例化连接器、servlet容器、Wrapper实例和其他组件,然后调用各个对象的set方法将它们关联起来;这种配置应用程序的方法有一个明显的缺陷,即所有的配置都必须硬编码。调整组件配置和属性值都必须要重新编译Bootstrap类。幸运的是,Tomcat的设计者使用了一种更加优雅的配置方式,即使用一个名为server.xml的XML文件来对应用程序进行配置。server.xml文件中的每个元素都会转换为一个java对象,元素的属性会用于设置java对象的属性,这样,就可以通过简单的编辑server.xml文件来修改tomcat的配置。

Tomcat使用了开源库Digester来将xml文件中的元素转换成java对象。

由于一个Context实例表示一个Web应用程序,因此配置Web应用程序是通过对已经实例化的Context实例进行配置完成的。用来配置Web应用程序的XML文件的名称是web.xml,该文件位于Web应用程序的WEB-INF目录下。

下面来介绍Digester库,Digester库是Apache软件基金会的Jatarta项目下的子Commons项目下的一个开源项目,它的主页地址是http://commons.apache.org/proper/commons-digester/

org.apache.commons.digester3.Digester类是Digester库中的主类,该类可用于解析XML文件,对于XML文件中的每个元素,Digester对象都会检查它是否要做事先预定义的事件,在调用Digester对象的parse()方法之前,程序员要先定义好Digester对象执行哪些动作。

因此,程序员要先定义好模式,然后将每个模式与一条或多条规则相关联。

模式通常是xml文件里面元素的路径,类似于xpath的语法路径

规则指明了当Digester对象遇到了某个特殊的模式时要执行的一个或多个动作,规则是org.apache.commons.digester3.Rule类的实例,Digester类开源包含0个或多个Rule对象,在Digester实例中,这些规则和其相关联的模式都存储在由org.apache.commons.digester3.Rules接口表示的一类存储器中,每当把一条规则添加到Digester实例中时,Rule对象都会被添加到Rules对象中。

另外,Rule类有begin()方法和end()方法,在解析xml文件时,当Digester实例遇到匹配某个模式的元素的开始标签时,它会调用相应的Rule对象的begin()方法,而当Digester实例遇到相应元素的结束标签时,它会调用Rule对象的end()方法。

在使用Digester库时,我们需要先导入相关依赖jar

<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-digester3</artifactId>
            <version>3.2</version>
            <classifier>with-deps</classifier>
        </dependency>

第一个示例应用程序演示如何使用Digester库动态的创建对象,并设置相应的属性值。

employee1.xml文件内容如下

<?xml version="1.0" encoding="ISO-8859-1"?>
<employee firstName="Brian" lastName="May">
</employee>

我们需要根据上面的xml文件创建Employee对象,并设置相应属性,Employee类代码如下:

public class Employee {
  private String firstName;
  private String lastName;
  private ArrayList offices = new ArrayList();
    
  public Employee() {
    System.out.println("Creating Employee");
  }
  public String getFirstName() {
    return firstName;
  }
  public void setFirstName(String firstName) {
    System.out.println("Setting firstName : " + firstName);
    this.firstName = firstName;
  }
  public String getLastName() {
    return lastName;
  }
  public void setLastName(String lastName) {
    System.out.println("Setting lastName : " + lastName);
    this.lastName = lastName;
  }
  public void addOffice(Office office) {
    System.out.println("Adding Office to this employee");
    offices.add(office);
  }
  public ArrayList getOffices() {
    return offices;
  }
  public void printName() {
    System.out.println("My name is " + firstName + " " + lastName);
  }
}

现在写一个测试类Test01,它使用Digester类,并为其添加创建Employee对象和设置其属性的规则。

public class Test01 {

    public static void main(String[] args) {
       
        InputStream inputStream = null;
        Digester digester = new Digester();
        // add rules
        digester.addObjectCreate("employee","ex15.pyrmont.digestertest.Employee");
        digester.addSetProperties("employee");
        digester.addCallMethod("employee", "printName");

        try {
            inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("employee1.xml");
            Employee employee = (Employee) digester.parse(inputStream);
            System.out.println("First name : " + employee.getFirstName());
            System.out.println("Last name : " + employee.getLastName());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

}

第二个示例演示如何利用Digester库创建两个对象,并建立他们之间的关系

employee2.xml 文件内容如下

<?xml version="1.0" encoding="ISO-8859-1"?>
<employee firstName="Freddie" lastName="Mercury">
  <office description="Headquarters">
    <address streetName="Wellington Avenue" streetNumber="223"/>
  </office>
  <office description="Client site">
    <address streetName="Downing Street" streetNumber="10"/>
  </office>
</employee>

然后我们还需要创建Office类和Address类

Office类代码如下:

public class Office {
  private Address address;
  private String description;
  public Office() {
    System.out.println("..Creating Office");
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    System.out.println("..Setting office description : " + description);
    this.description = description;
  }
  public Address getAddress() {
    return address;
  }
  public void setAddress(Address address) {
    System.out.println("..Setting office address : " + address);
    this.address = address;
  }
}

Address类代码如下:

public class Address {
  private String streetName;
  private String streetNumber;
  public Address() {
    System.out.println("....Creating Address");
  }
  public String getStreetName() {
    return streetName;
  }
  public void setStreetName(String streetName) {
    System.out.println("....Setting streetName : " + streetName);
    this.streetName = streetName;
  }
  public String getStreetNumber() {
    return streetNumber;
  }
  public void setStreetNumber(String streetNumber) {
    System.out.println("....Setting streetNumber : " + streetNumber);
    this.streetNumber = streetNumber;
  }
  public String toString() {
    return "...." + streetNumber + " " + streetName; 
  }
}

下面是Test02类的定义,该类使用一个Digester对象,并为其添加规则

public class Test02 {

    public static void main(String[] args) {
        
        InputStream inputStream = null;
        Digester digester = new Digester();
        // add rules
        digester.addObjectCreate("employee",
                "ex15.pyrmont.digestertest.Employee");
        digester.addSetProperties("employee");
        digester.addObjectCreate("employee/office",
                "ex15.pyrmont.digestertest.Office");
        digester.addSetProperties("employee/office");
        digester.addSetNext("employee/office", "addOffice");
        digester.addObjectCreate("employee/office/address",
                "ex15.pyrmont.digestertest.Address");
        digester.addSetProperties("employee/office/address");
        digester.addSetNext("employee/office/address", "setAddress");
        try {
            inputStream = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("employee2.xml");
            Employee employee = (Employee) digester.parse(inputStream);
            ArrayList offices = employee.getOffices();
            Iterator iterator = offices.iterator();
            System.out
                    .println("-------------------------------------------------");
            while (iterator.hasNext()) {
                Office office = (Office) iterator.next();
                Address address = office.getAddress();
                System.out.println(office.getDescription());
                System.out.println("Address : " + address.getStreetNumber()
                        + " " + address.getStreetName());
                System.out.println("--------------------------------");
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    }
}

Rule类包含了一些方法,其中最重要的两个方法是begin()方法和end()方法,当Digester实例遇到某个XML元素的开始标签时,它会调用它所包含的匹配Rule对象的begin()方法,方法签名如下:

public void begin( String namespace, String name, Attributes attributes ) throws Exception

当Digester实例遇到某个XML元素的结束标签时,它会调用它所包含的匹配Rule对象的end()方法,方法签名如下:

public void end( String namespace, String name ) throws Exception

Digester对象是如何完成这些工作的呢?当调用Digester对象的addObjectCreate()方法、addCallMethod()方法、addSetNext()方法或其他方法时,都会间接地调用Digester类的addRule()方法;该方法将一个Rule对象和它所匹配的模式添加到Digester对象的Rules集合中。

addRule()方法实现如下:

 public void addRule( String pattern, Rule rule )
    {
        rule.setDigester( this );
        getRules().add( pattern, rule );
    }

查看Digester类的addObjectCreate()方法的重载实现如下:

    public void addObjectCreate( String pattern, String className )
    {
        addRule( pattern, new ObjectCreateRule( className ) );
    }
   
    public void addObjectCreate( String pattern, Class<?> clazz )
    {
        addRule( pattern, new ObjectCreateRule( clazz ) );
    }
   
    public void addObjectCreate( String pattern, String className, String attributeName )
    {
        addRule( pattern, new ObjectCreateRule( className, attributeName ) );
    }
   
    public void addObjectCreate( String pattern, String attributeName, Class<?> clazz )
    {
        addRule( pattern, new ObjectCreateRule( attributeName, clazz ) );
    }

这四个重载方法都调用了addRule()方法,ObjectCreateRule类是Rule类的子类,该类的实例可作为addRule()方法的第二个参数使用。

下面是ObjectCreateRule类的begin()方法和end()方法的实现

 @Override
    public void begin( String namespace, String name, Attributes attributes )
        throws Exception
    {
        Class<?> clazz = this.clazz;

        if ( clazz == null )
        {
            // Identify the name of the class to instantiate
            String realClassName = className;
            if ( attributeName != null )
            {
                String value = attributes.getValue( attributeName );
                if ( value != null )
                {
                    realClassName = value;
                }
            }
            if ( getDigester().getLogger().isDebugEnabled() )
            {
                getDigester().getLogger().debug( format( "[ObjectCreateRule]{%s} New '%s'",
                                                         getDigester().getMatch(),
                                                         realClassName ) );
            }

            // Instantiate the new object and push it on the context stack
            clazz = getDigester().getClassLoader().loadClass( realClassName );
        }
        Object instance;
        if ( constructorArgumentTypes == null || constructorArgumentTypes.length == 0 )
        {
            if ( getDigester().getLogger().isDebugEnabled() )
            {
                getDigester()
                    .getLogger()
                    .debug( format( "[ObjectCreateRule]{%s} New '%s' using default empty constructor",
                                    getDigester().getMatch(),
                                    clazz.getName() ) );
            }

            instance = clazz.newInstance();
        }
        else
        {
            if ( proxyManager == null )
            {
                Constructor<?> constructor = getAccessibleConstructor( clazz, constructorArgumentTypes );

                if ( constructor == null )
                {
                    throw new SAXException(
                                   format( "[ObjectCreateRule]{%s} Class '%s' does not have a construcor with types %s",
                                           getDigester().getMatch(),
                                           clazz.getName(),
                                           Arrays.toString( constructorArgumentTypes ) ) );
                }
                proxyManager = new ProxyManager( clazz, constructor, defaultConstructorArguments, getDigester() );
            }
            instance = proxyManager.createProxy();
        }
        getDigester().push( instance );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void end( String namespace, String name )
        throws Exception
    {
        Object top = getDigester().pop();

        if ( proxyManager != null )
        {
            proxyManager.finalize( top );
        }

        if ( getDigester().getLogger().isDebugEnabled() )
        {
            getDigester().getLogger().debug( format( "[ObjectCreateRule]{%s} Pop '%s'",
                                                     getDigester().getMatch(),
                                                     top.getClass().getName() ) );
        }
    }

begin()方法用于创建一个对象实例,并将其压入到Digester对象的内部栈中;end()方法会将内部栈的栈顶元素弹出栈

要向Digester实例中添加Rule对象,还可以调用其addRuleSet()方法,方法实现如下:

public void addRuleSet( RuleSet ruleSet )
    {
        String oldNamespaceURI = getRuleNamespaceURI();
        String newNamespaceURI = ruleSet.getNamespaceURI();
        if ( log.isDebugEnabled() )
        {
            if ( newNamespaceURI == null )
            {
                log.debug( "addRuleSet() with no namespace URI" );
            }
            else
            {
                log.debug( "addRuleSet() with namespace URI " + newNamespaceURI );
            }
        }
        setRuleNamespaceURI( newNamespaceURI );
        ruleSet.addRuleInstances( this );
        setRuleNamespaceURI( oldNamespaceURI );
    }

org.apache.commons.digester3.RuleSet接口表示Rule对象的集合,该接口定义了两个方法,分别为addRuleInstance()和getNamespaceURI(),addRuleInstance()方法签名如下:

public void addRuleInstance(Digester digester)

addRuleInstance()方法用于添加定义在当前RuleSet对象中的Rule对象集合到作为该方法参数传输的Digester实例中

getNamespaceUR()方法返回将要应用在所有Rule对象(在当前Ruleset中创建的)的命名空间的URI,该方法签名如下

public java.lang.String getNamespaceURI()

因此,在创建了Digester对象之后,可以创建一个RuleSet对象,并将其传输给Digester对象的addRuleSet()方法

为了便于使用,实现RuleSet接口有一个基类RuleSetBase,RuleSetBase类为抽象类,提供了getNamespaceURI()方法的实现,我们只需要提供addRuleInstances()方法的实现就可以了

下面是我们创建的EmployeeRuleSet类的源码(继承自RuleSetBase类)

public class EmployeeRuleSet extends RuleSetBase  {
  public void addRuleInstances(Digester digester) {
    // add rules
    digester.addObjectCreate("employee", "ex15.pyrmont.digestertest.Employee");
    digester.addSetProperties("employee");    
    digester.addObjectCreate("employee/office", "ex15.pyrmont.digestertest.Office");
    digester.addSetProperties("employee/office");
    digester.addSetNext("employee/office", "addOffice");
    digester.addObjectCreate("employee/office/address", 
      "ex15.pyrmont.digestertest.Address");
    digester.addSetProperties("employee/office/address");
    digester.addSetNext("employee/office/address", "setAddress"); 
  }
}

我们注意到,EmployeeRuleSet类中的addRuleInstances()方法的实现的功能类似Test02类,将相同的Rule对象添加到Digester对象中

下面是Test03的代码,里面会创建EmployeeRuleSet类的实例,然后将其添加到之前创建的Digester对象中

public class Test03 {

  public static void main(String[] args) {
   
    InputStream inputStream = null;
    Digester digester = new Digester();
    digester.addRuleSet(new EmployeeRuleSet());
    try {
      inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("employee2.xml");
      Employee employee = (Employee) digester.parse(inputStream);
      ArrayList offices = employee.getOffices();
      Iterator iterator = offices.iterator();
      System.out.println("-------------------------------------------------");
      while (iterator.hasNext()) {
        Office office = (Office) iterator.next();
        Address address = office.getAddress();
        System.out.println(office.getDescription());
        System.out.println("Address : " + 
          address.getStreetNumber() + " " + address.getStreetName());
        System.out.println("--------------------------------");
      }
      
    }
    catch(Exception e) {
      e.printStackTrace();
    }
    finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
  }
}

与其他类型的容器不同,StandardContext实例必须有一个监听器,该监听器会负责配置StandardContext实例,设置成功后会将StandardContext实例的变量configued值设置为tue。

StandardContext类的标准监听器是org.apache.catalina.startup.ContextConfig类的实例,它会执行很对StandardContext实例来说必不可少的任务,例如安装验证器阀到StandardContext实例的管道对象中,此外还会添加许可器阀(类型为org.apache.catalina.valves.CertificateValve)到管道对象中。

但更重要的是,ContextConfig类的实例还会读取和解析默认的web.xml文件和应用程序自定义的web.xml文件,并将xml元素转换为java对象。

默认的web.xml文件位于CATALINE_HOME目录下的conf目录中,其中定义并映射了很多默认的servlet,配置了很多MIME类型文件的映射,定义了默认的session超时时间,以及定义了欢迎文件的列表。

应用程序的web.xml文件是应用程序自定义的配置文件,位于应用程序目录下的WEB-INF目录中。

ContextConfig实例会为每一个servlet元素创建StandardWrapper实例,因此,正如你在本章应用程序中看到的,配置变简单了,你不在需要实例化Wrapper实例了

因此,我们需要在Bootstrap类中实例化一个ContextConfig类,并调用org.apache.catalina.Lifecycle接口的addLifecycleListener()方法将其添加到StandardContext对象中

LifecycleListener listener = new ContextConfig();
((Lifecycle) context).addLifecycleListener(listener);

在启动和停止StandardContext实例时,会触发相应事件,ContextConfig类会对两种事件做出响应,分别为START_EVENT 和STOP_EVENT

每当StandardContext实例触发事件时,会调用ContextConfig实例的lifecycleEvent()方法

public void lifecycleEvent(LifecycleEvent event) {

        // Identify the context we are associated with
        try {
            context = (Context) event.getLifecycle();
            if (context instanceof StandardContext) {
                int contextDebug = ((StandardContext) context).getDebug();
                if (contextDebug > this.debug)
                    this.debug = contextDebug;
            }
        } catch (ClassCastException e) {
            log(sm.getString("contextConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.START_EVENT))
            start();
        else if (event.getType().equals(Lifecycle.STOP_EVENT))
            stop();

    }

在上面方法中,会继续调用start()方法和stop()方法

private synchronized void start() {

        if (debug > 0)
            log(sm.getString("contextConfig.start"));
        context.setConfigured(false);
        ok = true;

        // Set properties based on DefaultContext
        Container container = context.getParent();
        if( !context.getOverride() ) {
            if( container instanceof Host ) {
                ((Host)container).importDefaultContext(context);
                container = container.getParent();
            }
            if( container instanceof Engine ) {
                ((Engine)container).importDefaultContext(context);
            }
        }

        // Process the default and application web.xml files
        defaultConfig();
        applicationConfig();
        if (ok) {
            validateSecurityRoles();
        }

        // Scan tag library descriptor files for additional listener classes
        if (ok) {
            try {
                tldScan();
            } catch (Exception e) {
                log(e.getMessage(), e);
                ok = false;
            }
        }

        // Configure a certificates exposer valve, if required
        if (ok)
            certificatesConfig();

        // Configure an authenticator if we need one
        if (ok)
            authenticatorConfig();

        // Dump the contents of this pipeline if requested
        if ((debug >= 1) && (context instanceof ContainerBase)) {
            log("Pipline Configuration:");
            Pipeline pipeline = ((ContainerBase) context).getPipeline();
            Valve valves[] = null;
            if (pipeline != null)
                valves = pipeline.getValves();
            if (valves != null) {
                for (int i = 0; i < valves.length; i++) {
                    log("  " + valves[i].getInfo());
                }
            }
            log("======================");
        }

        // Make our application available if no problems were encountered
        if (ok)
            context.setConfigured(true);
        else {
            log(sm.getString("contextConfig.unavailable"));
            context.setConfigured(false);
        }

    }

start()方法会进一步调用defaultConfig()方法和applicationConfig()方法

defaultConfig()方法负责读取并解析位于%CATALINA_HOME%/conf目录下的默认的web.xml文件

private void defaultConfig() {

        // Open the default web.xml file, if it exists
        File file = new File(Constants.DefaultWebXml);
        if (!file.isAbsolute())
            file = new File(System.getProperty("catalina.base"),
                            Constants.DefaultWebXml);
        FileInputStream stream = null;
        try {
            stream = new FileInputStream(file.getCanonicalPath());
            stream.close();
            stream = null;
        } catch (FileNotFoundException e) {
            log(sm.getString("contextConfig.defaultMissing"));
            return;
        } catch (IOException e) {
            log(sm.getString("contextConfig.defaultMissing"), e);
            return;
        }

        // Process the default web.xml file
        synchronized (webDigester) {
            try {
                InputSource is =
                    new InputSource("file://" + file.getAbsolutePath());
                stream = new FileInputStream(file);
                is.setByteStream(stream);
                webDigester.setDebug(getDebug());
                if (context instanceof StandardContext)
                    ((StandardContext) context).setReplaceWelcomeFiles(true);
                webDigester.clear();
                webDigester.push(context);
                webDigester.parse(is);
            } catch (SAXParseException e) {
                log(sm.getString("contextConfig.defaultParse"), e);
                log(sm.getString("contextConfig.defaultPosition",
                                 "" + e.getLineNumber(),
                                 "" + e.getColumnNumber()));
                ok = false;
            } catch (Exception e) {
                log(sm.getString("contextConfig.defaultParse"), e);
                ok = false;
            } finally {
                try {
                    if (stream != null) {
                        stream.close();
                    }
                } catch (IOException e) {
                    log(sm.getString("contextConfig.defaultClose"), e);
                }
            }
        }

    }

applicationConfig()方法与defaultConfig()方法类似,只不过它处理的是应用程序自定义的部署描述符,该部署描述符位于应用目录下的WEB-INF目录中

private void applicationConfig() {

        // Open the application web.xml file, if it exists
        InputStream stream = null;
        ServletContext servletContext = context.getServletContext();
        if (servletContext != null)
            stream = servletContext.getResourceAsStream
                (Constants.ApplicationWebXml);
        if (stream == null) {
            log(sm.getString("contextConfig.applicationMissing"));
            return;
        }

        // Process the application web.xml file
        synchronized (webDigester) {
            try {
                URL url =
                    servletContext.getResource(Constants.ApplicationWebXml);

                InputSource is = new InputSource(url.toExternalForm());
                is.setByteStream(stream);
                webDigester.setDebug(getDebug());
                if (context instanceof StandardContext) {
                    ((StandardContext) context).setReplaceWelcomeFiles(true);
                }
                webDigester.clear();
                webDigester.push(context);
                webDigester.parse(is);
            } catch (SAXParseException e) {
                log(sm.getString("contextConfig.applicationParse"), e);
                log(sm.getString("contextConfig.applicationPosition",
                                 "" + e.getLineNumber(),
                                 "" + e.getColumnNumber()));
                ok = false;
            } catch (Exception e) {
                log(sm.getString("contextConfig.applicationParse"), e);
                ok = false;
            } finally {
                try {
                    if (stream != null) {
                        stream.close();
                    }
                } catch (IOException e) {
                    log(sm.getString("contextConfig.applicationClose"), e);
                }
            }
        }

    }

在ContextConfig类中,使用变量webDigester来引用一个Digester类型的对象

private static Digester webDigester = createWebDigester();

该Digester对象用于解析默认的web.xml文件和应用程序自定义的web.xml文件,在调用createWebDigester()方法时会添加用来处理web.xml文件的规则

/**
     * Create (if necessary) and return a Digester configured to process the
     * web application deployment descriptor (web.xml).
     */
    private static Digester createWebDigester() {

        URL url = null;
        Digester webDigester = new Digester();
        webDigester.setValidating(true);
        url = ContextConfig.class.getResource(Constants.WebDtdResourcePath_22);
        webDigester.register(Constants.WebDtdPublicId_22,
                             url.toString());
        url = ContextConfig.class.getResource(Constants.WebDtdResourcePath_23);
        webDigester.register(Constants.WebDtdPublicId_23,
                             url.toString());
        webDigester.addRuleSet(new WebRuleSet());
        return (webDigester);

    }

我们注意到,上面方法中调用了变量webDigester的addRuleSet()方法,传入一个org.apache.catalina.startup.WebRuleSet类型的对象作为参数;WebRuleSet类是org.apache.commons.digester.RuleSetBase的子类。

下面是WebRuleSet类的addRuleInstances()方法实现:

public void addRuleInstances(Digester digester) {

        digester.addRule(prefix + "web-app",
                         new SetPublicIdRule(digester, "setPublicId"));

        digester.addCallMethod(prefix + "web-app/context-param",
                               "addParameter", 2);
        digester.addCallParam(prefix + "web-app/context-param/param-name", 0);
        digester.addCallParam(prefix + "web-app/context-param/param-value", 1);

        digester.addCallMethod(prefix + "web-app/display-name",
                               "setDisplayName", 0);

        digester.addRule(prefix + "web-app/distributable",
                         new SetDistributableRule(digester));

        digester.addObjectCreate(prefix + "web-app/ejb-local-ref",
                                 "org.apache.catalina.deploy.ContextLocalEjb");
        digester.addSetNext(prefix + "web-app/ejb-local-ref",
                            "addLocalEjb",
                            "org.apache.catalina.deploy.ContextLocalEjb");

        //代码太长,后面部分略

    }

--------------------------------------------------------------------------- 

本系列How Tomcat Works系本人原创 

转载请注明出处 博客园 刺猬的温驯 

本人邮箱: chenying998179#163.com (#改为@

本文链接http://www.cnblogs.com/chenying99/p/3249161.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值