探秘Tomcat——启动篇
tomcat作为一款web服务器本身很复杂,代码量也很大,但是模块化很强,最核心的模块还是连接器Connector和容器Container。具体请看下图:
从图中可以看出
a. 高亮的两块是Connector和Container,为什么说他们两最核心,其中Connector是负责接收http请求,当你在浏览器中输入URL为http://www.demon.com,这样的http请求就会被Connector接收并转发给容器Container。
到了Container后,会经过不同的容器层有:
Engine:表示整个Catalina servlet引擎;
Host: 表示包含有一个或多个Context容器的虚拟主机;
Context: 表示一个Web应用程序,一个Context可以有多个Wrapper;
Wrapper: 表示一个独立的servlet
一层层的处理后最终到达的是Wrapper即特定的serlvet处理器,具体用于处理http的请求响应。
b. 图中除了Connector和Container以外还有一些Naming、Session等一些服务,统统封装在了一个service里面,可以用于对外提供各种服务,比如我们不仅希望 tomcat可以提供一个web请求响应的服务,还希望知道其中的详细处理细节,我们可以使用service中的日志服务,用于打印一些请求处理过程中的细节信息。
c. 有了这些service模块,我们还需要有一个落脚点,这个落脚点用于控制service的生杀大全,负责service的整个生命周期,包括service的初始化、启动、结束等等。
d. 综上举一个例子,现在有A软件公司,共有三个部门——研发部门、财务部门、技术支持部门
其中每个部门相当于一个service,在每个service中可以提供不同的服务,比如研发部门可以提供功能开发服务、功能测试服务、持续集成部署服务、美工服务等。
而封装在server中的各个服务由server来统一管理,好比A公司可以实现研发部门的组建(或重组)——开工——裁撤等一系列的生命周期功能,如果A公司逢上国家法定节假日需要放假休息,那么相关的service部门也都会执行放假模式,同理,如果A公司正常运营上班,那么各个service也都会切换到上班模式。有了server的存在,从而方便对于service的管理。
大致了解了tomcat的架构和工作原理,我们来看看平时我们通过点击startup.bat来启动tomcat是如何从代码层面实现的,在启动过程中又做了哪些事情(基于tomcat6版本的源码)。
1.启动入口
在代码中,tomcat的启动是通过运行org.apache.catalina.startup.Bootstrap类的main方法来启动服务的
public static void main(String args[]) {
if (daemon == null) {
daemon = new Bootstrap();
try {
daemon.init();
} catch (Throwable t) {
t.printStackTrace();
return;
}
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else {
log.warn("Bootstrap: command "" + command + "" does not exist.");
}
} catch (Throwable t) {
t.printStackTrace();
}
}
a. 初始化Bootstrap对象
b. 根据具体的需求完成服务的加载、启动和关闭的功能
备注:这里运行或调试main方法的时候需要在VM arguments中填入类似-Dcatalina.home="C:UsersAdministratorDesktoptomcatapache-tomcat-6.0.43-srcoutputbuild"这样的参数,具体操作参见《探秘Tomcat(一)——Myeclipse中导入Tomcat源码》
2.Bootstrap的初始化
2.1 init方法
public void init()
throws Exception
{
// Set Catalina path
setCatalinaHome();
setCatalinaBase();
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
a. 其中setCatalinaHome和setCatalinaBase分别用于为catalina.name和catalina.base赋值;
b. initClassLoaders用于初始类加载器,分别初始化了三个加载器CommonLoader、CatalinaLoader和SharedLoader,从代码可以发现CommonLoader是CatalinaLoader和SharedLoader的父类,最终初始化完成,两个子类都指向CommonLoader;
c. 通过反射机制生成一份org.apache.catalina.startup.Catalina的实例用于启动。
2.2 createClassLoader方法
下面我们来看看init->initClassLoaders->createClassLoader这个方法,通过这个方法我们分别得到了三个加载器CommonLoader、CatalinaLoader和SharedLoader,我们加载所需要的目录和jar包等。
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");//得到的value为${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
if ((value == null) || (value.equals("")))
return parent;
ArrayList repositoryLocations = new ArrayList();
ArrayList repositoryTypes = new ArrayList();
int i;
StringTokenizer tokenizer = new StringTokenizer(value, ",");//用于分隔String的类
while (tokenizer.hasMoreElements()) {//遍历value中的值
String repository = tokenizer.nextToken();
// Local repository
boolean replace = false;
String before = repository;
while ((i=repository.indexOf(CATALINA_HOME_TOKEN))>=0) {//这样的遍历说明如果value的每项里面包含了${catalina.home}或者${catalina.base}就将标记变量replace置为true,并且确确实实的替换这些变量,比如这里讲${catalina.base}替换为C:UsersAdministratorDesktoptomcatapache-tomcat-6.0.43-srcoutputbuild。每次遍历完value值后,都会根据存储的repository的类型添加不同的值到repositoryTypes中去。
replace=true;
if (i>0) {
repository = repository.substring(0,i) + getCatalinaHome()
+ repository.substring(i+CATALINA_HOME_TOKEN.length());
} else {
repository = getCatalinaHome()
+ repository.substring(CATALINA_HOME_TOKEN.length());
}
}
while ((i=repository.indexOf(CATALINA_BASE_TOKEN))>=0) {
replace=true;
if (i>0) {
repository = repository.substring(0,i) + getCatalinaBase()
+ repository.substring(i+CATALINA_BASE_TOKEN.length());
} else {
repository = getCatalinaBase()
+ repository.substring(CATALINA_BASE_TOKEN.length());
}
}
if (replace && log.isDebugEnabled())
log.debug("Expanded " + before + " to " + repository);
// Check for a JAR URL repository
try {
URL url=new URL(repository);
repositoryLocations.add(repository);
repositoryTypes.add(ClassLoaderFactory.IS_URL);
continue;
} catch (MalformedURLException e) {
// Ignore
}
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositoryLocations.add(repository);
repositoryTypes.add(ClassLoaderFactory.IS_GLOB);
} else if (repository.endsWith(".jar")) {
repositoryLocations.add(repository);
repositoryTypes.add(ClassLoaderFactory.IS_JAR);
} else {
repositoryLocations.add(repository);
repositoryTypes.add(ClassLoaderFactory.IS_DIR);
}
}
String[] locations = (String[]) repositoryLocations.toArray(new String[0]);
Integer[] types = (Integer[]) repositoryTypes.toArray(new Integer[0]);//分别将两个list集合转化为数组
ClassLoader classLoader = ClassLoaderFactory.createClassLoader
(locations, types, parent);//调用createClassLoader方法,其中细加载到每个目录和具体的目录下面的文件,注意其中的加载的数据集使用LinkedHashSet,这是利用了set集合的特性,不允许重复元素的出现,因为这里面会有重复加载的情况,所以用set保证了唯一性。通过这个操作等于加载了所有的文件,具体类加载以及URLClassLoader的使用可以参见http://blog.csdn.net/mycomputerxiaomei/article/details/24470465
// Retrieving MBean server
MBeanServer mBeanServer = null;//得到一个MBean的对象
if (MBeanServerFactory.findMBeanServer(null).size() > 0) {
mBeanServer =
(MBeanServer) MBeanServerFactory.findMBeanServer(null).get(0);
} else {
mBeanServer = ManagementFactory.getPlatformMBeanServer();
}
// Register the server classloader
ObjectName objectName =
new ObjectName("Catalina:type=ServerClassLoader,name=" + name);
mBeanServer.registerMBean(classLoader, objectName);//将类加载器注册到mbean服务上,这样做的作用是,一旦classLoader有变化了,就会有notification。
return classLoader;
}
这里有个小细节在createClassLoader方法中有调用方法getCatalinaBase,而getCatalinaBase又会去调用getCatalinaHome,而不是将getCatalinaHome中的具体功能代码又冗余的写一遍,这是一个很好的习惯^_^
至此,我们完成了Bootstrap对象的初始化,为catalina.home等,初始化了三个类加载器。
3.server的加载和启动
3.1 deamon.load方法
public void load() {
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Create and execute our Digester
Digester digester = createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource("file://" + file.getAbsolutePath());
} catch (Exception e) {
;
}
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
;
}
}
// This should be included in catalina.jar
// Alternative: don't bother with xml, just create it manually.
if( inputStream==null ) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
;
}
}
if ((inputStream == null) && (file != null)) {
log.warn("Can't load server.xml from " + file.getAbsolutePath());
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
return;
}
try {
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
inputStream.close();
} catch (Exception e) {
log.warn("Catalina.start using "
+ getConfigFile() + ": " , e);
return;
}
// Stream redirection
initStreams();
// Start the new server
if (getServer() instanceof Lifecycle) {
try {
getServer().initialize();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new java.lang.Error(e);
else
log.error("Catalina.start", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled())
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
a. 前面的完成一些初始化的工作;
b. createStartDigester方法很重要,用于声明要实例化的所有服务,当然这个工作需要与file = configFile();中声明的位于conf下的server.xml进行配合;
c. 通过digester.parse(inputSource)解析server.xml中的元素
protected Digester createStartDigester() {
long t1=System.currentTimeMillis();
// Initialize the digester
Digester digester = new Digester();
digester.setValidating(false);
digester.setRulesValidation(true);
HashMap<Class, List<String>> fakeAttributes = new HashMap<Class, List<String>>();
ArrayList<String> attrs = new ArrayList<String>();
attrs.add("className");
fakeAttributes.put(Object.class, attrs);
digester.setFakeAttributes(fakeAttributes);
digester.setClassLoader(StandardServer.class.getClassLoader());
// Configure the actions we will be using
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
digester.addObjectCreate("Server/GlobalNamingResources",
"org.apache.catalina.deploy.NamingResources");
digester.addSetProperties("Server/GlobalNamingResources");
digester.addSetNext("Server/GlobalNamingResources",
"setGlobalNamingResources",
"org.apache.catalina.deploy.NamingResources");
digester.addObjectCreate("Server/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties("Server/Listener");
digester.addSetNext("Server/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
digester.addObjectCreate("Server/Service",
"org.apache.catalina.core.StandardService",
"className");
digester.addSetProperties("Server/Service");
digester.addSetNext("Server/Service",
"addService",
"org.apache.catalina.Service");
digester.addObjectCreate("Server/Service/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties("Server/Service/Listener");
digester.addSetNext("Server/Service/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
//Executor
digester.addObjectCreate("Server/Service/Executor",
"org.apache.catalina.core.StandardThreadExecutor",
"className");
digester.addSetProperties("Server/Service/Executor");
digester.addSetNext("Server/Service/Executor",
"addExecutor",
"org.apache.catalina.Executor");
digester.addRule("Server/Service/Connector",
new ConnectorCreateRule());
digester.addRule("Server/Service/Connector",
new SetAllPropertiesRule(new String[]{"executor"}));
digester.addSetNext("Server/Service/Connector",
"addConnector",
"org.apache.catalina.connector.Connector");
digester.addObjectCreate("Server/Service/Connector/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties("Server/Service/Connector/Listener");
digester.addSetNext("Server/Service/Connector/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
// Add RuleSets for nested elements
digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
digester.addRuleSet(new EngineRuleSet("Server/Service/"));
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));
digester.addRuleSet(ClusterRuleSetFactory.getClusterRuleSet("Server/Service/Engine/Host/Cluster/"));
digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));
// When the 'engine' is found, set the parentClassLoader.
digester.addRule("Server/Service/Engine",
new SetParentClassLoaderRule(parentClassLoader));
digester.addRuleSet(ClusterRuleSetFactory.getClusterRuleSet("Server/Service/Engine/Cluster/"));
long t2=System.currentTimeMillis();
if (log.isDebugEnabled())
log.debug("Digester for server.xml created " + ( t2-t1 ));
return (digester);
}
从这里我们看到了很多熟悉的字眼比如StandardServer、Engine、Host、Context等,可以对比看下server.xml中的标签
1 <?xml version='1.0' encoding='utf-8'?>
2 <!--
3 Licensed to the Apache Software Foundation (ASF) under one or more
4 contributor license agreements. See the NOTICE file distributed with
5 this work for additional information regarding copyright ownership.
6 The ASF licenses this file to You under the Apache License, Version 2.0
7 (the "License"); you may not use this file except in compliance with
8 the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing, software
13 distributed under the License is distributed on an "AS IS" BASIS,
14 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 See the License for the specific language governing permissions and
16 limitations under the License.
17 -->
18 <!-- Note: A "Server" is not itself a "Container", so you may not
19 define subcomponents such as "Valves" at this level.
20 Documentation at /docs/config/server.html
21 -->
22 <Server port="8005" shutdown="SHUTDOWN">
23
24 <!--APR library loader. Documentation at /docs/apr.html -->
25 <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
26 <!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->
27 <Listener className="org.apache.catalina.core.JasperListener" />
28 <!-- Prevent memory leaks due to use of particular java/javax APIs-->
29 <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
30 <!-- JMX Support for the Tomcat server. Documentation at /docs/non-existent.html -->
31 <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" />
32 <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
33
34 <!-- Global JNDI resources
35 Documentation at /docs/jndi-resources-howto.html
36 -->
37 <GlobalNamingResources>
38 <!-- Editable user database that can also be used by
39 UserDatabaseRealm to authenticate users
40 -->
41 <Resource name="UserDatabase" auth="Container"
42 type="org.apache.catalina.UserDatabase"
43 description="User database that can be updated and saved"
44 factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
45 pathname="conf/tomcat-users.xml" />
46 </GlobalNamingResources>
47
48 <!-- A "Service" is a collection of one or more "Connectors" that share
49 a single "Container" Note: A "Service" is not itself a "Container",
50 so you may not define subcomponents such as "Valves" at this level.
51 Documentation at /docs/config/service.html
52 -->
53 <Service name="Catalina">
54
55 <!--The connectors can use a shared executor, you can define one or more named thread pools-->
56 <!--
57 <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
58 maxThreads="150" minSpareThreads="4"/>
59 -->
60
61
62 <!-- A "Connector" represents an endpoint by which requests are received
63 and responses are returned. Documentation at :
64 Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
65 Java AJP Connector: /docs/config/ajp.html
66 APR (HTTP/AJP) Connector: /docs/apr.html
67 Define a non-SSL HTTP/1.1 Connector on port 8080
68 -->
69 <Connector port="8080" protocol="HTTP/1.1"
70 connectionTimeout="20000"
71 redirectPort="8443" />
72 <!-- A "Connector" using the shared thread pool-->
73 <!--
74 <Connector executor="tomcatThreadPool"
75 port="8080" protocol="HTTP/1.1"
76 connectionTimeout="20000"
77 redirectPort="8443" />
78 -->
79 <!-- Define a SSL HTTP/1.1 Connector on port 8443
80 This connector uses the JSSE configuration, when using APR, the
81 connector should be using the OpenSSL style configuration
82 described in the APR documentation -->
83 <!--
84 <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
85 maxThreads="150" scheme="https" secure="true"
86 clientAuth="false" sslProtocol="TLS" />
87 -->
88
89 <!-- Define an AJP 1.3 Connector on port 8009 -->
90 <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
91
92
93 <!-- An Engine represents the entry point (within Catalina) that processes
94 every request. The Engine implementation for Tomcat stand alone
95 analyzes the HTTP headers included with the request, and passes them
96 on to the appropriate Host (virtual host).
97 Documentation at /docs/config/engine.html -->
98
99 <!-- You should set jvmRoute to support load-balancing via AJP ie :
100 <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
101 -->
102 <Engine name="Catalina" defaultHost="localhost">
103
104 <!--For clustering, please take a look at documentation at:
105 /docs/cluster-howto.html (simple how to)
106 /docs/config/cluster.html (reference documentation) -->
107 <!--
108 <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
109 -->
110
111 <!-- The request dumper valve dumps useful debugging information about
112 the request and response data received and sent by Tomcat.
113 Documentation at: /docs/config/valve.html -->
114 <!--
115 <Valve className="org.apache.catalina.valves.RequestDumperValve"/>
116 -->
117
118 <!-- This Realm uses the UserDatabase configured in the global JNDI
119 resources under the key "UserDatabase". Any edits
120 that are performed against this UserDatabase are immediately
121 available for use by the Realm. -->
122 <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
123 resourceName="UserDatabase"/>
124
125 <!-- Define the default virtual host
126 Note: XML Schema validation will not work with Xerces 2.2.
127 -->
128 <Host name="localhost" appBase="webapps"
129 unpackWARs="true" autoDeploy="true"
130 xmlValidation="false" xmlNamespaceAware="false">
131
132 <!-- SingleSignOn valve, share authentication between web applications
133 Documentation at: /docs/config/valve.html -->
134 <!--
135 <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
136 -->
137
138 <!-- Access log processes all example.
139 Documentation at: /docs/config/valve.html -->
140 <!--
141 <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
142 prefix="localhost_access_log." suffix=".txt" pattern="common" resolveHosts="false"/>
143 -->
144
145 </Host>
146 </Engine>
147 </Service>
148 </Server>
Digester就是根据web.xml中声明的标签生成对象,并把元素的属性赋值为对象的属性,并关联起他们之间的父子关系。
3.2 start方法
既然已经加载好了server以及所需要的service,那么就可以开始启动了。
public void start()
throws Exception {
if( catalinaDaemon==null ) init();
Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
method.invoke(catalinaDaemon, (Object [])null);
}
通过反射机制,可是调用Catalina类下面的start方法。
下面我们看看Catalina中的start方法具体都做了什么
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
long t1 = System.nanoTime();
// Start the new server
if (getServer() instanceof Lifecycle) {
try {
((Lifecycle) getServer()).start();
} catch (LifecycleException e) {
log.error("Catalina.start: ", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled())
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
try {
// Register shutdown hook
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
} catch (Throwable t) {
// This will fail on JDK 1.2. Ignoring, as Tomcat can run
// fine without the shutdown hook.
}
if (await) {
await();
stop();
}
}
a. 首先判断这个server是否为空,如果为空则需要先加载load
b. 判断getServer是否是Lifecycle的实例,显然是,我们可以从StandardServer类的声明中
“public final class StandardServer implements Lifecycle, Server, MBeanRegistration”可以看出,StandardServer实现了LifeCycle的接口,Server接口。
而且当我们进入Server类的时候,可以看到类的注释上写了:一般而且如果某类实现了Server接口,同时也要实现LifeCycle接口,这也正好验证了这里StandardServer的声明;
c. 如果满足是LifeCycle的实例的条件,则执行StandardServer中的start方法,该方法主要用于启动所有前面解析出来的service,包括进入类Connector启动Connector服务,进入Container中依次通过Engine、Host、Context和Wrapper启动相应的服务。
至此,就完成了
- Bootstrap的初始化
- 加载server服务
- 启动server服务
最终实现了启动tomcat的目的,其实现在回头来看,启动一个服务器无非就是启动了一个server^^
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。
- weblogic启动失败:Could not obtain the localhost address 解决办法
- 如何理解云计算?很简单,就像吃货想吃披萨了……
- .NET 2.0 中使用Active Directory 应用程序模式 (ADAM)
- struts2: 通过流输出实现exce导出
- Google的数据交换协议:GData (Google Data APIs Protocol)
- C# 内部类
- 四字母.com域名均以五位数结拍
- mybatis 3.2.7 与 spring mvc 3.x、logback整合
- spring 3.2.x + struts2 + mybatis 3.x + logback 整合配置
- struts2使用Convention Plugin在weblogic上以war包部署时,找不到Action的解决办法
- 使用xfce4桌面系统
- 号外!号外!Python纳入高考内容了!人工智能时代就要来临了!
- 高颜值!域名5h.net和jb.cc纷纷易主
- 认识ASP.NET 5项目结构和项目文件xproj
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 2015年javaB组1-4题解析与理解
- 【matplotlib】绘制散点图
- LeetCode刷题心得 -- map的妙用
- 2015javaB组第五题表格计算
- 【线性回归】读取txt
- 走近STL - Vector,再次见面
- 【线性回归】标准方程法
- 133: error: in C++98 XXX must be initialized by constructor, not by '{...}'
- spring之如何将验证错误信息显示在相应界面
- 【python-leetcode329-深度优先搜索】矩阵中的最长递增路径
- 带权树 -- 哈夫曼树,与它的那张哈夫曼编码表
- 【python-动态规划】0-1背包问题
- python之列表推导和生成器表达式
- 二叉树的前中后序遍历
- 【tensorflow2.0】处理结构化数据-titanic生存预测