- 浏览: 244028 次
- 性别:
- 来自: 北京
-
文章分类
最新评论
-
feifei435:
不错的文章,但是有一点忘了提,创建了新的sessionid后, ...
Tomcat的Session管理(一) - Session的生成 -
yangcheng33:
博主你为什么这么棒!!!
Tomcat的Session管理(一) - Session的生成 -
u010323779:
哈哈,非常不错,昨天看了一天源代码,不知从何入手,现在看来您的 ...
Tomcat的Session管理(二) - Session后台处理 -
hdwmp123:
...
Tomcat请求处理(一) -- 服务器端口监听 -
tinguo002:
...
Tomcat的Session管理(一) - Session的生成
二. 载入
2. Bootstrap的#Bootstrap#load(String[] arguments)方法。
方法的源代码如下述所示:
这时就该轮到查看Catalina的两个load()方法了。先看看带参数的版本:
代码很短,在内部还调用了无参数的load()版本。在这之前,先来看一下arguments()方法:
主要对可以使用的一些参数进行了一些判断,如果参数有误就返回false。
如果arguments()返回true,两个分支就合二为一了,都进入了无参数的load()。
下面来重点看下这个方法:
initDirs()和initNaming()主要是值的设定,就不细说了。
createStartDigester()方法主要是实例化一个Digester对象,并提供了xml中的模式与java类对象的绑定。对Digester并没有什么研究,只是通过JavaDoc大致了解了下程序的运作过程。
虽然没有研究Digester,但是大致的过程还是可以猜出来的,首先,Digester需要一个堆栈的顶端元素,在Catalina#load()里压入了Catalina类本身,后边会介绍到,然后会根据一系列addObjectCreate,addSetProperties,addSetNext设定类与xml的绑定等。比如GlobalNamingResources这个属性,
上边的3条语句设定了遇到Server/GlobalNamingResources,实例化一个NamingResources类的对象,并调用setGlobalNamingResources将这个对象与父对象关联。而它的父对象很明显就是"Server"定义的对象,就是StandardServer类的对象。而StandardServer又与栈定对象相关联(通过调用Catalina的#setServer设定的)。所以解析完server.xml之后,Catalina的成员变量"server(定义为:protected Server server = null;)"就应该被赋值了,并且xml中的所有元素在server下边都有对应的对象相关联。
这里有必要介绍下server.xml中定义的各个元素的作用(引用自http://jackycheng2007.iteye.com/blog/188401)
Server
"Server" 是单例的,代表整个JVM,它可能包含几个"Service"实例。 "Server" 从指定的端口监听关闭命令。
<Server port="8005" shutdown="SHUTDOWN">
"Service"
一个"Service"是一个或者多个"Connectors"的集合,他们共享一个"Container"(所以多个web应用在整个容器内是可见的)。通常但不必须,Container是一个"Engine"。
<Service name="Catalina">
可以把Service看成媒介,它存活在一个Server里面,把一个或几个Connectors绑定在一个Engine上。
所以,在server.xml 中"Service"是Server的子组件。Connector 和 Engine 是Service的子组件。
"Connector"
一个Connector将在某个指定端口上侦听客户请求,并将获得的请求交给Engine来处理,从Engine处获得回应并返回客户
TOMCAT有两个典型的Connector,一个直接侦听来自browser的http请求,一个侦听来自其它WebServer的请求
Coyote Http/1.1 Connector 在端口8080处侦听来自客户browser的http请求
Coyote JK2 Connector 在端口8009处侦听来自其它WebServer(Apache)的servlet/jsp代理请求.
<Connector port="8080" protocol="HTTP/1.1" maxThreads="150" connectionTimeout="20000" redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
"Engine"
一个"Engine"代表了一个service的请求管道。一个service可能有多个Connector, 这个Engine接收并处理来自这些Connector的请求(requests ),并且将处理反馈(response )传给相应的connector,进而传给客户端。
"Host "
一个Host 是域名(e.g. www.yourcompany.com)和tomcat server的联系体。 一个Engine可以包含多个Host,而且Host 可以支持网络别名如yourcompany.com 和 abc.yourcompany.com。所以,Host是Engine的子组件。
"Context"
一个Context代表一个web应用。一个Host可以包含多个 Context, 他们都有一个唯一的路径。
再回到代码。
digester初始化结束后,就是一系列的server.xml的寻找工作了,当定位了server.xml之后,就要开始对它的解析了。
这一段就是解析的代码,很明显,digester把Catalina对象压到了栈顶。
然后,调用了这样的一句话:server.initialize();前面已经提到过了,server对象已经被实力化为StandardServer类的对象,那么看一下StandardServer#initialize()都做了什么:
首先,lifecycle.fireLifecycleEvent(INIT_EVENT, null);通知了StandardServer包含的事件监听器有一个"INIT_EVENT"事件到来了,默认有如下四个监听器:
org.apache.catalina.core.AprLifecycleListener)(Apache Portable Runtime -- APR)
org.apache.catalina.core.JasperListener(Java Server Pages -- JSP)
org.apache.catalina.mbeans.ServerLifecycleListener
org.apache.catalina.mbeans.GlobalResourcesLifecycleListener
四个监听器的INIT_EVENT方法处理最后涉及到了native方法的调用,就不再详细说了。
然后,对JMX进行了初始化,JMX暂时不懂。
最后,对Service进行了初始化,默认只有一个StandardService#initialize()
通过上述的步骤,Tomcat的ServerSocket已经建立了起来,load的过程也结束了。
2. Bootstrap的#Bootstrap#load(String[] arguments)方法。
方法的源代码如下述所示:
private void load(String[] arguments) throws Exception { // 要调用的方法名 String methodName = "load"; // 参数名 Object param[]; // 参数值 Class paramTypes[]; if (arguments == null || arguments.length == 0) { paramTypes = null; param = null; } else { paramTypes = new Class[1]; paramTypes[0] = arguments.getClass(); param = new Object[1]; param[0] = arguments; } Method method = catalinaDaemon.getClass().getMethod(methodName, paramTypes); if (log.isDebugEnabled()) log.debug("Calling startup class " + method); // 这里会使程序产生两个分支,调用Catalina#load()和Catalina#load(String args[])两个方法。 method.invoke(catalinaDaemon, param); }
这时就该轮到查看Catalina的两个load()方法了。先看看带参数的版本:
public void load(String args[]) { try { if (arguments(args)) load(); } catch (Exception e) { e.printStackTrace(System.out); } }
代码很短,在内部还调用了无参数的load()版本。在这之前,先来看一下arguments()方法:
protected boolean arguments(String args[]) { boolean isConfig = false; if (args.length < 1) { usage(); return (false); } for (int i = 0; i < args.length; i++) { if (isConfig) { // server.xml的路径 configFile = args[i]; isConfig = false; } else if (args[i].equals("-config")) { // 下一个参数是server.xml的路径 isConfig = true; } else if (args[i].equals("-nonaming")) { // 取消naming支持 setUseNaming(false); } else if (args[i].equals("-help")) { // 帮助 usage(); return (false); } else if (args[i].equals("start")) { // 开启 starting = true; stopping = false; } else if (args[i].equals("stop")) { // 关闭 starting = false; stopping = true; } else { usage(); return (false); } } return (true); }
主要对可以使用的一些参数进行了一些判断,如果参数有误就返回false。
如果arguments()返回true,两个分支就合二为一了,都进入了无参数的load()。
下面来重点看下这个方法:
public void load() { long t1 = System.nanoTime(); // 初始化Catalina环境路径,CatalinaHome和CatalinaBase initDirs(); // 初始化naming initNaming(); // 创建一个Digester对象,主要用于xml文件的读取,本实例中的Digester用于server.xml的读取。 // 在createStartDigester()中队server.xml中的xml样式与具体的类进行了绑定。 Digester digester = createStartDigester(); InputSource inputSource = null; InputStream inputStream = null; File file = null; try { // server.xml文件 file = configFile(); inputStream = new FileInputStream(file); inputSource = new InputSource("file://" + file.getAbsolutePath()); } catch (Exception e) { ; } if (inputStream == null) { try { // 在jar包中寻找server.xml inputStream = getClass().getClassLoader().getResourceAsStream(getConfigFile()); inputSource = new InputSource(getClass().getClassLoader().getResource( getConfigFile()).toString()); } catch (Exception e) { ; } } if (inputStream == null) { try { // 在jar包中寻找config-embed.xml 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()); return; } try { inputSource.setByteStream(inputStream); digester.push(this); digester.parse(inputSource); inputStream.close(); } catch (Exception e) { log.warn("Catalina.start using " + getConfigFile() + ": ", e); return; } // 重定向输出流和错误流 initStreams(); if (server instanceof Lifecycle) { try { // Server初始化 server.initialize(); } catch (LifecycleException e) { log.error("Catalina.start", e); } } long t2 = System.nanoTime(); if (log.isInfoEnabled()) log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms"); }
initDirs()和initNaming()主要是值的设定,就不细说了。
createStartDigester()方法主要是实例化一个Digester对象,并提供了xml中的模式与java类对象的绑定。对Digester并没有什么研究,只是通过JavaDoc大致了解了下程序的运作过程。
protected Digester createStartDigester() { long t1 = System.currentTimeMillis(); // 初始化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()); // 三个参数分别为:1. xml标签的名字;2. 默认的Java类名;3.如果标签中有这个属性,那么它就覆盖掉默认的Java类名 // 具体到下边的这条语句,就是处理"Server"标签,并实例化一个StandardServer类的实例 // 如果Server标签有一个叫className的属性,就会覆盖掉默认的Java类型。 digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className"); // 设定一个SetPropertiesRule,这个规则是用于堆栈顶端元素的属性设置 digester.addSetProperties("Server"); // 三个参数分别为:1.xml标签名;2.要设定的"父对象"(与继承无关)的方法名;3.方法参数的Java类型 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, "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, "className"); digester.addSetProperties("Server/Service/Listener"); digester.addSetNext("Server/Service/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); 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, "className"); digester.addSetProperties("Server/Service/Connector/Listener"); digester.addSetNext("Server/Service/Connector/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); 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/")); 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); }
虽然没有研究Digester,但是大致的过程还是可以猜出来的,首先,Digester需要一个堆栈的顶端元素,在Catalina#load()里压入了Catalina类本身,后边会介绍到,然后会根据一系列addObjectCreate,addSetProperties,addSetNext设定类与xml的绑定等。比如GlobalNamingResources这个属性,
digester.addObjectCreate("Server/GlobalNamingResources", "org.apache.catalina.deploy.NamingResources"); digester.addSetProperties("Server/GlobalNamingResources"); digester.addSetNext("Server/GlobalNamingResources", "setGlobalNamingResources", "org.apache.catalina.deploy.NamingResources");
上边的3条语句设定了遇到Server/GlobalNamingResources,实例化一个NamingResources类的对象,并调用setGlobalNamingResources将这个对象与父对象关联。而它的父对象很明显就是"Server"定义的对象,就是StandardServer类的对象。而StandardServer又与栈定对象相关联(通过调用Catalina的#setServer设定的)。所以解析完server.xml之后,Catalina的成员变量"server(定义为:protected Server server = null;)"就应该被赋值了,并且xml中的所有元素在server下边都有对应的对象相关联。
这里有必要介绍下server.xml中定义的各个元素的作用(引用自http://jackycheng2007.iteye.com/blog/188401)
Server
"Server" 是单例的,代表整个JVM,它可能包含几个"Service"实例。 "Server" 从指定的端口监听关闭命令。
<Server port="8005" shutdown="SHUTDOWN">
"Service"
一个"Service"是一个或者多个"Connectors"的集合,他们共享一个"Container"(所以多个web应用在整个容器内是可见的)。通常但不必须,Container是一个"Engine"。
<Service name="Catalina">
可以把Service看成媒介,它存活在一个Server里面,把一个或几个Connectors绑定在一个Engine上。
所以,在server.xml 中"Service"是Server的子组件。Connector 和 Engine 是Service的子组件。
"Connector"
一个Connector将在某个指定端口上侦听客户请求,并将获得的请求交给Engine来处理,从Engine处获得回应并返回客户
TOMCAT有两个典型的Connector,一个直接侦听来自browser的http请求,一个侦听来自其它WebServer的请求
Coyote Http/1.1 Connector 在端口8080处侦听来自客户browser的http请求
Coyote JK2 Connector 在端口8009处侦听来自其它WebServer(Apache)的servlet/jsp代理请求.
<Connector port="8080" protocol="HTTP/1.1" maxThreads="150" connectionTimeout="20000" redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
"Engine"
一个"Engine"代表了一个service的请求管道。一个service可能有多个Connector, 这个Engine接收并处理来自这些Connector的请求(requests ),并且将处理反馈(response )传给相应的connector,进而传给客户端。
"Host "
一个Host 是域名(e.g. www.yourcompany.com)和tomcat server的联系体。 一个Engine可以包含多个Host,而且Host 可以支持网络别名如yourcompany.com 和 abc.yourcompany.com。所以,Host是Engine的子组件。
"Context"
一个Context代表一个web应用。一个Host可以包含多个 Context, 他们都有一个唯一的路径。
再回到代码。
digester初始化结束后,就是一系列的server.xml的寻找工作了,当定位了server.xml之后,就要开始对它的解析了。
try { inputSource.setByteStream(inputStream); digester.push(this); digester.parse(inputSource); inputStream.close(); } catch (Exception e) { log.warn("Catalina.start using " + getConfigFile() + ": ", e); return; }
这一段就是解析的代码,很明显,digester把Catalina对象压到了栈顶。
然后,调用了这样的一句话:server.initialize();前面已经提到过了,server对象已经被实力化为StandardServer类的对象,那么看一下StandardServer#initialize()都做了什么:
public void initialize() throws LifecycleException { if (initialized) { log.info(sm.getString("standardServer.initialize.initialized")); return; } // 通知所有Lifecycle事件监听器"INIT_EVENT"事件的发生 lifecycle.fireLifecycleEvent(INIT_EVENT, null); initialized = true; // JMX相关的配置 if (oname == null) { try { oname = new ObjectName("Catalina:type=Server"); Registry.getRegistry(null, null).registerComponent(this, oname, null); } catch (Exception e) { log.error("Error registering ", e); } } try { ObjectName oname2 = new ObjectName(oname.getDomain() + ":type=StringCache"); Registry.getRegistry(null, null).registerComponent(new StringCache(), oname2, null); } catch (Exception e) { log.error("Error registering ", e); } // Service初始化,默认只有一个org.apache.catalina.core.StandardService实例 // 见Catalina#createStartDigester() for (int i = 0; i < services.length; i++) { services[i].initialize(); } }
首先,lifecycle.fireLifecycleEvent(INIT_EVENT, null);通知了StandardServer包含的事件监听器有一个"INIT_EVENT"事件到来了,默认有如下四个监听器:
org.apache.catalina.core.AprLifecycleListener)(Apache Portable Runtime -- APR)
org.apache.catalina.core.JasperListener(Java Server Pages -- JSP)
org.apache.catalina.mbeans.ServerLifecycleListener
org.apache.catalina.mbeans.GlobalResourcesLifecycleListener
四个监听器的INIT_EVENT方法处理最后涉及到了native方法的调用,就不再详细说了。
然后,对JMX进行了初始化,JMX暂时不懂。
最后,对Service进行了初始化,默认只有一个StandardService#initialize()
public void initialize() throws LifecycleException { if (initialized) { if (log.isInfoEnabled()) log.info(sm.getString("standardService.initialize.initialized")); return; } initialized = true; if (oname == null) { try { // 容器 Container engine = this.getContainer(); // 名字,默认是Catalina domain = engine.getName(); oname = new ObjectName(domain + ":type=Service,serviceName=" + name); this.controller = oname; // 注册JMX,我发誓看完这个就去看JMX Registry.getRegistry(null, null).registerComponent(this, oname, null); // Executors的注册 Executor[] executors = findExecutors(); for (int i = 0; i < executors.length; i++) { ObjectName executorObjectName = new ObjectName(domain + ":type=Executor,name=" + executors[i].getName()); Registry.getRegistry(null, null).registerComponent(executors[i], executorObjectName, null); } } catch (Exception e) { log.error(sm.getString("standardService.register.failed", domain), e); } } if (server == null) { ServerFactory.getServer().addService(this); } synchronized (connectors) { // 这里是Connector的初始化,默认有HTTP/1.1(8080),AJP/1.3(8009)两个 // <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> // <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> for (int i = 0; i < connectors.length; i++) { // ServerSocket初始化 connectors[i].initialize(); } } }
通过上述的步骤,Tomcat的ServerSocket已经建立了起来,load的过程也结束了。
发表评论
-
Tomcat NIO源代码分析(三) -- Protocol和Processor
2010-12-09 09:39 3051现在请求到了Protocol(Http11NioProtoco ... -
Tomcat NIO源代码分析(二) -- Poller
2010-12-08 08:46 4526接着上面的流程,现在请求到了Poller的#register( ... -
Tomcat NIO源代码分析(一) -- Acceptor
2010-12-07 09:11 6844这里主要讲一下Tomcat使用NIO启动和进行请求处理的大致流 ... -
Tomcat的Session管理(二) - Session后台处理
2009-01-06 18:57 11047Tomcat会开启一个后台线程每隔一段时间检查Session的 ... -
Tomcat的Session管理(一) - Session的生成
2009-01-06 17:03 38407Session对象的创建一般是源于这样的一条语句: Sess ... -
Tomcat请求处理(七) - Servlet实例的调用
2009-01-05 17:09 5492Tomcat请求处理中Servlet实例的调用是和Filter ... -
Tomcat请求处理(六) -- Servlet实例创建
2009-01-04 19:02 4958首先,来看一下Servlet的载入过程。 具体是在org.a ... -
Tomcat请求处理(五) -- 请求在容器间的流动
2008-12-28 13:54 4022请求在Tomcat中传到了CoyoteAdapter的#ser ... -
Tomcat请求处理(四) -- Request, Response, 和Pipeline
2008-12-25 18:24 35631. Request和Response 当处理请求的时候,T ... -
Tomcat请求处理(三) -- coyote请求处理
2008-12-19 00:51 6251在上一篇文章文章中,Tomcat的请求处理到了JIoEndpo ... -
Tomcat请求处理(二) -- 请求处理框架
2008-11-03 10:05 3833书接上文。 当Tomcat的Acceptor监听到有请求到来 ... -
Tomcat请求处理(一) -- 服务器端口监听
2008-10-31 15:14 6849其实tomcat在哪个类中监听请求的代码很容易找到: 在or ... -
Tomcat启动部分源代码分析(五) -- 应用程序加载
2008-10-30 19:10 2504前面所叙述的tomcat启动中并没有webapps下边应用程序 ... -
Tomcat启动部分源代码分析(四) -- 开启容器
2008-10-28 12:23 2336四. 开启容器 最后是Bootstrap#start()方法 ... -
Tomcat启动部分源代码分析(二) -- 初始化
2008-10-28 12:17 5540二. 初始化 1. 首先是Boo ... -
Tomcat启动部分源代码分析(一) -- 概览
2008-10-28 12:10 3780一. 概览 本文所涉及的Tomcat为6.0版本。 Tom ...
相关推荐
9. "Tomcat启动源代码分析.pdf":深入到启动脚本和Java代码,解释了从启动脚本开始,如何初始化和启动Tomcat服务的全过程。 10. "tomcat类加载机制.pdf":再次聚焦于Tomcat的类加载机制,可能深入到更多细节和技巧...
这可能包括Tomcat的配置文件(如server.xml、web.xml)、Tomcat的可执行文件(如catalina.sh或catalina.bat)、以及一个名为"HelloWorld"的Web应用目录,该目录下有Servlet的源代码(HelloWorld.java)、编译后的类...
创建项目目录结构,包括`src`目录用于存放Java源代码,`war`目录用于存放JSP文件、配置文件等。这一步强调了良好的目录结构对于项目的管理非常重要。 - **第2步:创建index.jsp** 在`war`目录中创建初始的JSP...
Spring的核心优势在于其非侵入式设计,能够与其他框架无缝集成,提高代码的可读性和可维护性。 #### 二、开发第一个Spring程序 ##### 1. 开发环境搭建 - **软件需求**:需安装JDK1.4.2或以上版本,Tomcat5.0+,...
- 从官方网站或可靠的源下载最新的 Struts 和 Spring 发布版本。 2. **创建项目目录和 ant build 文件**: - 使用 Equinox 工具来简化项目的搭建过程。Equinox 是一个预配置好的开发环境,包括了 Struts、Spring ...
JSP 在线学习系统 是一套完善的web设计系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。 二、功能介绍 有管理员,教师和学生三个角色。管理员负责载入和更新教师...
- **项目目录结构与ant build文件**:Equinox已经定义好了基本的目录结构,例如src/main/java用于存放Java源代码,src/main/webapp用于存放Web相关的资源文件等。此外,Equinox还提供了一套完整的ant build脚本,...
Spring的applicationContext.xml文件是Spring容器的核心,它负责载入应用程序的配置信息,包括数据源、事务管理器等。 在业务层中,我们将使用Spring来设置业务代理(businessdelegates)和数据访问对象(DAO)的依赖性...