标签:
step1)首先介绍Host接口及其 相关实现类 StandardHost类, StandardHostMapper 类 和 StandardHostValve类;step2)接下来使用一个应用程序来说明如何使用Host实例作为一个顶层servlet容器;step3)介绍Engine接口 及其 相关实现类 StandardEngine 和 StandardEngineValve类,并使用一个app 说明如何使用Engine实例作为顶层 servlet容器;
public interface Host extends Container { // org.apache.catalina.Host public static final String ADD_ALIAS_EVENT = "addAlias"; public static final String REMOVE_ALIAS_EVENT = "removeAlias"; public String getAppBase(); public void setAppBase(String appBase); public boolean getAutoDeploy(); public void setAutoDeploy(boolean autoDeploy); public void addDefaultContext(DefaultContext defaultContext); public DefaultContext getDefaultContext(); public String getName(); public void setName(String name); public void importDefaultContext(Context context); public void addAlias(String alias); public String[] findAliases(); public Context map(String uri); // highlight line. public void removeAlias(String alias); }
public class StandardHost extends ContainerBase implements Deployer, Host { //org.apache.catalina.core.StandardHost public StandardHost() { super(); pipeline.setBasic(new StandardHostValve()); }2)调用其start()方法时,会添加两个阀,分别是ErrorReportValve类和ErrorDispatchValve类的实例。start()方法源代码如下:
public synchronized void start() throws LifecycleException { //org.apache.catalina.core.StandardHost.start(). // Set error report valve if ((errorReportValveClass != null) && (!errorReportValveClass.equals(""))) { try { Valve valve = (Valve) Class.forName(errorReportValveClass) .newInstance(); addValve(valve); // highlight line. } catch (Throwable t) { log(sm.getString ("standardHost.invalidErrorReportValveClass", errorReportValveClass)); } } // Set dispatcher valve addValve(new ErrorDispatcherValve()); // highlight line. super.start(); // highlight line. } // private String errorReportValveClass = "org.apache.catalina.valves.ErrorReportValve"; // defined in StandardHost
public synchronized void addValve(Valve valve) { //org.apache.catalina.core.ContainerBase.addValve(). pipeline.addValve(valve); fireContainerEvent(ADD_VALVE_EVENT, valve); }
public Context map(String uri) { //org.apache.catalina.core.StandardHost.map(). if (debug > 0) log("Mapping request URI '" + uri + "'"); if (uri == null) return (null); // Match on the longest possible context path prefix if (debug > 1) log(" Trying the longest context path prefix"); Context context = null; String mapuri = uri; while (true) { context = (Context) findChild(mapuri); if (context != null) break; int slash = mapuri.lastIndexOf('/'); if (slash < 0) break; mapuri = mapuri.substring(0, slash); } // If no Context matches, select the default Context if (context == null) { if (debug > 1) log(" Trying the default context"); context = (Context) findChild(""); } // Complain if no Context has been selected if (context == null) { log(sm.getString("standardHost.mappingError", uri)); return (null); } // Return the mapped Context (if any) if (debug > 0) log(" Mapped to context '" + context.getPath() + "'"); return (context); }
protected void addDefaultMapper(String mapperClass) { //org.apache.catalina.core.ContainerBase.addDefaultMapper(). if (mapperClass == null) return; if (mappers.size() >= 1) return; // Instantiate and add a default Mapper try { Class clazz = Class.forName(mapperClass); Mapper mapper = (Mapper) clazz.newInstance(); mapper.setProtocol("http"); addMapper(mapper); } catch (Exception e) { log(sm.getString("containerBase.addDefaultMapper", mapperClass), e); } }
A1)mapperClass的值定义在 StandardHost 类中:private String mapperClass = "org.apache.catalina.core.StandardHostMapper"; //defined in org.apache.catalina.core.StandardHostA2)StandardHost.start()方法:会在方法末尾调用父类的 start()方法(ContainerBase.start()方法),确保默认映射器创建完成;public synchronized void start() throws LifecycleException { //org.apache.catalina.core.ContainerBase.start(). // Validate and update our current component state if (started) throw new LifecycleException (sm.getString("containerBase.alreadyStarted", logName())); // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); addDefaultMapper(this.mapperClass); started = true; // Start our subordinate components, if any if ((loader != null) && (loader instanceof Lifecycle)) ((Lifecycle) loader).start(); if ((logger != null) && (logger instanceof Lifecycle)) ((Lifecycle) logger).start(); if ((manager != null) && (manager instanceof Lifecycle)) ((Lifecycle) manager).start(); if ((cluster != null) && (cluster instanceof Lifecycle)) ((Lifecycle) cluster).start(); if ((realm != null) && (realm instanceof Lifecycle)) ((Lifecycle) realm).start(); if ((resources != null) && (resources instanceof Lifecycle)) ((Lifecycle) resources).start(); // Start our Mappers, if any Mapper mappers[] = findMappers(); for (int i = 0; i < mappers.length; i++) { if (mappers[i] instanceof Lifecycle) ((Lifecycle) mappers[i]).start(); } // Start our child containers, if any Container children[] = findChildren(); for (int i = 0; i < children.length; i++) { if (children[i] instanceof Lifecycle) ((Lifecycle) children[i]).start(); } // Start the Valves in our pipeline (including the basic), if any if (pipeline instanceof Lifecycle) ((Lifecycle) pipeline).start(); // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(START_EVENT, null); // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }
public Container map(Request request, boolean update) { //org.apache.catalina.core.StandardHostMapper.map() // Has this request already been mapped? if (update && (request.getContext() != null)) return (request.getContext()); // Perform mapping on our request URI String uri = ((HttpRequest) request).getDecodedRequestURI(); Context context = host.map(uri); // highlight line. // Update the request (if requested) and return the selected Context if (update) { request.setContext(context); if (context != null) ((HttpRequest) request).setContextPath(context.getPath()); else ((HttpRequest) request).setContextPath(null); } return (context); }
public void invoke(Request request, Response response, ValveContext valveContext) //org.apache.catalina.core.StandardHostValve.invoke(). throws IOException, ServletException { // Validate the request and response object types if (!(request.getRequest() instanceof HttpServletRequest) || !(response.getResponse() instanceof HttpServletResponse)) { return; // NOTE - Not much else we can do generically } // Select the Context to be used for this Request StandardHost host = (StandardHost) getContainer(); Context context = (Context) host.map(request, true); // highlight line. if (context == null) { ((HttpServletResponse) response.getResponse()).sendError (HttpServletResponse.SC_INTERNAL_SERVER_ERROR, sm.getString("standardHost.noContext")); return; } // Bind the context CL to the current thread Thread.currentThread().setContextClassLoader (context.getLoader().getClassLoader()); // Update the session last access time for our session (if any) HttpServletRequest hreq = (HttpServletRequest) request.getRequest(); String sessionId = hreq.getRequestedSessionId(); if (sessionId != null) { Manager manager = context.getManager(); if (manager != null) { Session session = manager.findSession(sessionId); if ((session != null) && session.isValid()) session.access(); //highlight line. } } // Ask this Context to process this request context.invoke(request, response); Thread.currentThread().setContextClassLoader (StandardHostValve.class.getClassLoader()); }
step1)在tomcat4中, 首先invoke()方法会调用 StandardHost.map() 方法来获取一个相应的 Context实例;// Select the Context to be used for this Request StandardHost host = (StandardHost) getContainer(); Context context = (Context) host.map(request, true); // highlight line.
Attention)在获取Context实例的代码中有一个往复的过程。上面的map()方法需要两个参数,该方法定义在 ContainerBase类中。ContainerBase.map()方法会找到其子容器的映射器,并调用其map()方法;
step2)然后,invoke()方法会获取与该request对象相关联的session 对象,并调用其 access()方法。access()方法会修改 session对象的最后访问时间。step3)org.apache.catalina.session.StandardSession.access()方法的源代码如下:public void access() { this.isNew = false; this.lastAccessedTime = this.lastUsedTime; this.lastUsedTime = System.currentTimeMillis(); }step4)最后StandardHostValve.invoke()方法调用Context实例的 invoke()来处理http 请求;
r1)使用ContextConfig对象 需要知道应用程序 web.xml 文件的位置。在其 applicationConfig() 方法中,它会试图打开web.xml ;applicationConfig()方法的源码片段如下:private void applicationConfig() { // org.apache.catalina.startup.ContextConfig.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); // public static final String ApplicationWebXml = "/WEB-INF/web.xml"; if (stream == null) { log(sm.getString("contextConfig.applicationMissing")); return; } // Process the application web.xml file synchronized (webDigester) { // highlight code begins. 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); webDigester.push(null); } 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); } } } }<strong style="font-family: SimSun; background-color: rgb(255, 255, 255);"> </strong>
A1)其中public static final String ApplicationWebXml = "/WEB-INF/web.xml";web.xml 文件的相对路径,servletContext是一个 org.apache.catalina.core.ApplicationContext类型(实现了 javax.servlet.servletContext接口)的对象;A2)下面是 ApplicationContext.getResource()方法的部分实现代码:public URL getResource(String path) throws MalformedURLException { // org.apache.catalina.core.ApplicationContext.getResource(). path = normalize(path); if (path == null) return (null); DirContext resources = context.getResources(); if (resources != null) { String fullPath = context.getName() + path; String hostName = context.getParent().getName(); //highlight line. try { resources.lookup(path); if( System.getSecurityManager() != null ) { try { PrivilegedGetResource dp = new PrivilegedGetResource (hostName, fullPath, resources); return (URL)AccessController.doPrivileged(dp); } catch( PrivilegedActionException pe) { throw pe.getException(); } } else { return new URL ("jndi", null, 0, getJNDIUri(hostName, fullPath), new DirContextURLStreamHandler(resources)); } } catch (Exception e) { //e.printStackTrace(); } } return (null); }Attention)上述“highlight line” 所标识的行表明了,如果要使用 ContextConfig实例来进行配置的话,Context实例必须有一个 Host实例作为其父容器,否则context.getParent().getName()会抛出异常导致获取资源文件(web.xml)的URL不成功;
public final class Bootstrap1 { public static void main(String[] args) { //invoke: http://localhost:8080/app1/Primitive or http://localhost:8080/app1/Modern System.setProperty("catalina.base", System.getProperty("user.dir")); Connector connector = new HttpConnector(); Wrapper wrapper1 = new StandardWrapper(); wrapper1.setName("Primitive"); //wrapper1.setServletClass("servlet.PrimitiveServlet"); wrapper1.setServletClass("PrimitiveServlet"); Wrapper wrapper2 = new StandardWrapper(); wrapper2.setName("Modern"); //wrapper2.setServletClass("servlet.ModernServlet"); wrapper2.setServletClass("ModernServlet"); Context context = new StandardContext(); // StandardContext's start method adds a default mapper context.setPath("/app1"); context.setDocBase("app1"); context.addChild(wrapper1); context.addChild(wrapper2); LifecycleListener listener = new SimpleContextConfig(); ((Lifecycle) context).addLifecycleListener(listener); Host host = new StandardHost(); host.addChild(context); host.setName("localhost"); host.setAppBase("webapps"); Loader loader = new WebappLoader(); context.setLoader(loader); // context.addServletMapping(pattern, name); context.addServletMapping("/Primitive", "Primitive"); context.addServletMapping("/Modern", "Modern"); connector.setContainer(host); try { connector.initialize(); ((Lifecycle) connector).start(); ((Lifecycle) host).start(); // 与以往的Bootstrap.java不同的是,这里是host.start() 而不是 context.start() // make the application wait until we press a key. System.in.read(); ((Lifecycle) host).stop(); } catch (Exception e) { e.printStackTrace(); } } }
E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src>java -cp .;lib/servlet.jar;lib/catalina_4_1_24.jar;lib/catalina-5.5.4.jar;lib/naming-common jar;lib/commons-collections.jar;lib/naming-resources.jar;lib/commons-digester.jar;lib/catalina.jar;E:\bench-cluster\cloud-data-preprocess\HowTomcatWo ks\webroot com.tomcat.chapter13.startup.Bootstrap1 HttpConnector Opening server socket on all host IP addresses HttpConnector[8080] Starting background thread WebappLoader[/app1]: Deploying class repositories to work directory E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src\work\_\localhost\app1 StandardManager[/app1]: Seeding random number generator class java.security.SecureRandom StandardManager[/app1]: Seeding of random number generator has been completed StandardManager[/app1]: IOException while loading persisted sessions: java.io.EOFException java.io.EOFException // <span class="comment" style="margin: 0px; padding: 0px; border: none; color: rgb(0, 130, 0); font-family: Consolas, 'Courier New', Courier, mono, serif; line-height: 18px;">这是从文件中加载 session对象到内存,由于没有相关文件,所以加载失败,抛出异常,但这不会影响我们访问servlet,大家不要惊慌; </span><span style="margin: 0px; padding: 0px; border: none; font-family: Consolas, 'Courier New', Courier, mono, serif; line-height: 18px;"> </span> at java.io.ObjectInputStream$PeekInputStream.readFully(Unknown Source) at java.io.ObjectInputStream$BlockDataInputStream.readShort(Unknown Source) at java.io.ObjectInputStream.readStreamHeader(Unknown Source) at java.io.ObjectInputStream.<init>(Unknown Source) at org.apache.catalina.util.CustomObjectInputStream.<init>(CustomObjectInputStream.java:103) at org.apache.catalina.session.StandardManager.load(StandardManager.java:408) at org.apache.catalina.session.StandardManager.start(StandardManager.java:655) at org.apache.catalina.core.StandardContext.start(StandardContext.java:3570) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardHost.start(StandardHost.java:738) at com.tomcat.chapter13.startup.Bootstrap1.main(Bootstrap1.java:59) StandardManager[/app1]: Exception loading sessions from persistent storage java.io.EOFException at java.io.ObjectInputStream$PeekInputStream.readFully(Unknown Source) at java.io.ObjectInputStream$BlockDataInputStream.readShort(Unknown Source) at java.io.ObjectInputStream.readStreamHeader(Unknown Source) at java.io.ObjectInputStream.<init>(Unknown Source) at org.apache.catalina.util.CustomObjectInputStream.<init>(CustomObjectInputStream.java:103) at org.apache.catalina.session.StandardManager.load(StandardManager.java:408) at org.apache.catalina.session.StandardManager.start(StandardManager.java:655) at org.apache.catalina.core.StandardContext.start(StandardContext.java:3570) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardHost.start(StandardHost.java:738) at com.tomcat.chapter13.startup.Bootstrap1.main(Bootstrap1.java:59) ModernServlet -- init StandardHost[localhost]: MAPPING configuration error for request URI /favicon.ico
public interface Engine extends Container { public String getDefaultHost(); public void setDefaultHost(String defaultHost); public String getJvmRoute(); public void setJvmRoute(String jvmRouteId); public Service getService(); public void setService(Service service); public void addDefaultContext(DefaultContext defaultContext); public DefaultContext getDefaultContext(); public void importDefaultContext(Context context); }
public class StandardEngine extends ContainerBase implements Engine { public StandardEngine() { super(); pipeline.setBasic(new StandardEngineValve()); }2)作为一个顶层容器,Engine容器可以有子容器,而它的子容器只能是Host容器;将一个非Host容器设置为其子容器,则抛出异常,参见StandardEngine.addChild():
public void addChild(Container child) { // org.apache.catalina.core.StandardEngine.addChild(). if (!(child instanceof Host)) throw new IllegalArgumentException (sm.getString("standardEngine.notHost")); super.addChild(child); }
public void setParent(Container container) { // org.apache.catalina.core.StandardEngine.setParent(). throw new IllegalArgumentException (sm.getString("standardEngine.notParent")); }
public void invoke(Request request, Response response, ValveContext valveContext) // org.apache.catalina.core.StandardEngineValve.invoke(). throws IOException, ServletException { // Validate the request and response object types if (!(request.getRequest() instanceof HttpServletRequest) || !(response.getResponse() instanceof HttpServletResponse)) { return; // NOTE - Not much else we can do generically } // Validate that any HTTP/1.1 request included a host header HttpServletRequest hrequest = (HttpServletRequest) request; if ("HTTP/1.1".equals(hrequest.getProtocol()) && (hrequest.getServerName() == null)) { ((HttpServletResponse) response.getResponse()).sendError (HttpServletResponse.SC_BAD_REQUEST, sm.getString("standardEngine.noHostHeader", request.getRequest().getServerName())); return; } // Select the Host to be used for this Request StandardEngine engine = (StandardEngine) getContainer(); Host host = (Host) engine.map(request, true); if (host == null) { ((HttpServletResponse) response.getResponse()).sendError (HttpServletResponse.SC_BAD_REQUEST, sm.getString("standardEngine.noHost", request.getRequest().getServerName())); return; } // Ask this Host to process this request host.invoke(request, response); }
step1)在验证了request和response对象的类型后,invoke()方法得到Host实例,用于处理该请求;step2)invoke()方法会通过调用Engine实例的map()方法获取Host对象;step3)得到Host对象后,调用其invoke() 方法处理请求;
public final class Bootstrap2 { public static void main(String[] args) { //invoke: http://localhost:8080/app1/Primitive or http://localhost:8080/app1/Modern System.setProperty("catalina.base", System.getProperty("user.dir")); Connector connector = new HttpConnector(); Wrapper wrapper1 = new StandardWrapper(); wrapper1.setName("Primitive"); //wrapper1.setServletClass("servlet.PrimitiveServlet"); wrapper1.setServletClass("PrimitiveServlet"); Wrapper wrapper2 = new StandardWrapper(); wrapper2.setName("Modern"); //wrapper2.setServletClass("servlet.ModernServlet"); wrapper2.setServletClass("ModernServlet"); Context context = new StandardContext(); // StandardContext's start method adds a default mapper context.setPath("/app1"); context.setDocBase("app1"); context.addChild(wrapper1); context.addChild(wrapper2); LifecycleListener listener = new SimpleContextConfig(); ((Lifecycle) context).addLifecycleListener(listener); Host host = new StandardHost(); host.addChild(context); host.setName("localhost"); host.setAppBase("webapps"); Loader loader = new WebappLoader(); context.setLoader(loader); // context.addServletMapping(pattern, name); context.addServletMapping("/Primitive", "Primitive"); context.addServletMapping("/Modern", "Modern"); Engine engine = new StandardEngine(); engine.addChild(host); engine.setDefaultHost("localhost"); connector.setContainer(engine); try { connector.initialize(); ((Lifecycle) connector).start(); ((Lifecycle) engine).start(); // make the application wait until we press a key. System.in.read(); ((Lifecycle) engine).stop(); // 与以往不同的是,这里是engine.start()而不是 context.start(),因为最上层容易已经改变了,而enginge包含host,host包含context,context包含wrapper. } catch (Exception e) { e.printStackTrace(); } } }
step1)当http 请求到达 tomcat server时,HttpConnector得到 ServerSocket((accept()方法)),然后调用最上层容器的invoke()方法,而所有容器都继承自ContainerBase,所以实际上调用的是 ContainerBase.invoke()方法;step2)之后调用管道StandardPipeline.invoke()方法,进而调用管道的非基础阀和基础阀的invoke方法(而基础阀在容器的构造器中设置了);(干货——所有容器都继承自ContainerBase,而管道StandardPipeline是在 ContainerBase中创建的,所以所有容器都共用同一个管道对象,而当调用到某容器的时候,设置其对应的基础阀即可)step3)基础阀的invoke()方法会调用下一层级容器的invoke方法,接着继续调用管道StandardPipeline.invoke()方法,一直进行下去(回到step2的过程).......直到到达Wrapper容器(因为它是最小的容器,是一个具体servlet的封装);step4)Wrapper.invoke()方法,同样也要调用其管道StandardPipeline.invoke()方法,接着调用基础阀StandardWrapperValve,与其他基础阀不同的是,StandardWrapperValve.invoke()方法会调用 ApplicationFilterChain.doFilter()方法,接着调用具体的HttpServlet.allocate()方法和service()方法,写出响应info(html)到client,ending(Bingo);
E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src>java -cp .;lib/servlet.jar;lib/catalina_4_1_24.jar;lib/catalina-5.5.4.jar;lib/naming-common. jar;lib/commons-collections.jar;lib/naming-resources.jar;lib/commons-digester.jar;lib/catalina.jar;E:\bench-cluster\cloud-data-preprocess\HowTomcatWok s\webroot com.tomcat.chapter13.startup.Bootstrap2 HttpConnector Opening server socket on all host IP addresses HttpConnector[8080] Starting background thread Apache Tomcat/4.1.24 WebappLoader[/app1]: Deploying class repositories to work directory E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src\work\_\localhost\app1 WebappLoader[/app1]: Deploy class files /WEB-INF/classes to E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src\webapps\app1\WEB-INF\classes StandardManager[/app1]: Seeding random number generator class java.security.SecureRandom StandardManager[/app1]: Seeding of random number generator has been completed StandardManager[/app1]: IOException while loading persisted sessions: java.io.EOFException java.io.EOFException at java.io.ObjectInputStream$PeekInputStream.readFully(Unknown Source) at java.io.ObjectInputStream$BlockDataInputStream.readShort(Unknown Source) at java.io.ObjectInputStream.readStreamHeader(Unknown Source) at java.io.ObjectInputStream.<init>(Unknown Source) at org.apache.catalina.util.CustomObjectInputStream.<init>(CustomObjectInputStream.java:103) at org.apache.catalina.session.StandardManager.load(StandardManager.java:408) at org.apache.catalina.session.StandardManager.start(StandardManager.java:655) at org.apache.catalina.core.StandardContext.start(StandardContext.java:3570) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardHost.start(StandardHost.java:738) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:347) at com.tomcat.chapter13.startup.Bootstrap2.main(Bootstrap2.java:67) StandardManager[/app1]: Exception loading sessions from persistent storage java.io.EOFException at java.io.ObjectInputStream$PeekInputStream.readFully(Unknown Source) at java.io.ObjectInputStream$BlockDataInputStream.readShort(Unknown Source) at java.io.ObjectInputStream.readStreamHeader(Unknown Source) at java.io.ObjectInputStream.<init>(Unknown Source) at org.apache.catalina.util.CustomObjectInputStream.<init>(CustomObjectInputStream.java:103) at org.apache.catalina.session.StandardManager.load(StandardManager.java:408) at org.apache.catalina.session.StandardManager.start(StandardManager.java:655) at org.apache.catalina.core.StandardContext.start(StandardContext.java:3570) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardHost.start(StandardHost.java:738) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1188) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:347) at com.tomcat.chapter13.startup.Bootstrap2.main(Bootstrap2.java:67) ModernServlet -- init StandardHost[localhost]: MAPPING configuration error for request URI /favicon.ico StandardHost[localhost]: MAPPING configuration error for request URI /favicon.ico
标签:
原文地址:http://blog.csdn.net/pacosonswjtu/article/details/51459804