引言
Apache Tomcat 作为最流行的 Java Web 应用服务器之一,其启动过程涉及多个组件的初始化和协调工作。深入理解 Tomcat 的启动流程,不仅有助于我们更好地进行性能调优和故障排查,还能为定制化开发提供重要参考。本文将从脚本执行开始,详细剖析 Tomcat 的完整启动过程。
Tomcat 架构概览
在深入启动过程之前,我们先了解 Tomcat 的核心架构组件:
- Server:代表整个 Tomcat 服务器实例
- Service:包含一个或多个 Connector 和一个 Engine
- Connector:处理客户端连接和请求
- Engine:处理请求的 Servlet 引擎
- Host:虚拟主机
- Context:Web 应用程序
- Wrapper:Servlet 的包装器
启动脚本执行流程
1. startup.sh/startup.bat 脚本
Tomcat 的启动通常从执行 startup.sh(Linux/Unix)或 startup.bat(Windows)开始:
#!/bin/sh
# startup.sh 核心内容
PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh
exec "$PRGDIR"/"$EXECUTABLE" start "$@"该脚本主要完成:
- 确定 Tomcat 安装目录
- 设置必要的环境变量
- 调用
catalina.sh脚本
2. catalina.sh 脚本详解
catalina.sh 是 Tomcat 启动的核心脚本,负责:
# 设置 CATALINA_HOME 和 CATALINA_BASE
if [ -z "$CATALINA_HOME" ] ; then
CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd`
fi
# 设置 Java 相关参数
if [ -z "$JAVA_HOME" ] && [ -z "$JRE_HOME" ]; then
# 查找 Java 环境
JAVA_PATH=`which java 2>/dev/null`
fi
# 构建启动命令
eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" \
$JAVA_OPTS $CATALINA_OPTS \
-classpath "\"$CLASSPATH\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" start关键环境变量:
CATALINA_HOME:Tomcat 安装目录CATALINA_BASE:Tomcat 实例目录JAVA_OPTS:JVM 参数CATALINA_OPTS:Tomcat 特定参数
Bootstrap 类初始化
Bootstrap.main() 方法
Java 进程启动后,首先执行 org.apache.catalina.startup.Bootstrap.main() 方法:
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// 创建 Bootstrap 实例
Bootstrap bootstrap = new Bootstrap();
try {
// 初始化类加载器和 Catalina 实例
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
// 处理命令参数
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
}
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
}
}类加载器体系初始化
Tomcat 使用自定义的类加载器体系来实现应用隔离:
private void initClassLoaders() {
try {
// 创建 common 类加载器
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
commonLoader = this.getClass().getClassLoader();
}
// 创建 server 类加载器
catalinaLoader = createClassLoader("server", commonLoader);
// 创建 shared 类加载器
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}类加载器层次结构:
Catalina 组件初始化
Catalina.load() 方法
Bootstrap 通过反射调用 Catalina.load() 方法:
public void load() {
// 初始化 JMX
initNaming();
// 解析 server.xml
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(
Bootstrap.getCatalinaBaseFile(), getConfigFile()));
File file = configFile();
// 创建 Digester 解析器
Digester digester = createStartDigester();
try (ConfigurationSource.Resource resource =
ConfigFileLoader.getSource().getServerXml()) {
InputStream inputStream = resource.getInputStream();
InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
} catch (Exception e) {
log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e);
return;
}
// 初始化 Server
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// 调用 Server 的 init 方法
try {
getServer().init();
} catch (LifecycleException e) {
log.error(sm.getString("catalina.initError"), e);
}
}server.xml 解析过程
Tomcat 使用 Digester(基于 SAX)解析 server.xml 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context path="" docBase="ROOT" />
</Host>
</Engine>
</Service>
</Server>解析规则示例:
protected Digester createStartDigester() {
Digester digester = new Digester();
digester.setValidating(false);
digester.setRulesValidation(true);
// Server 规则
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server", "setServer",
"org.apache.catalina.Server");
// Service 规则
digester.addObjectCreate("Server/Service",
"org.apache.catalina.core.StandardService",
"className");
digester.addSetProperties("Server/Service");
digester.addSetNext("Server/Service", "addService",
"org.apache.catalina.Service");
// Connector 规则
digester.addObjectCreate("Server/Service/Connector",
"org.apache.catalina.connector.Connector",
"className");
digester.addSetProperties("Server/Service/Connector");
digester.addSetNext("Server/Service/Connector", "addConnector",
"org.apache.catalina.connector.Connector");
return digester;
}生命周期管理机制
Lifecycle 接口
Tomcat 的所有核心组件都实现了 Lifecycle 接口:
public interface Lifecycle {
// 生命周期事件
public static final String BEFORE_INIT_EVENT = "before_init";
public static final String AFTER_INIT_EVENT = "after_init";
public static final String START_EVENT = "start";
public static final String BEFORE_START_EVENT = "before_start";
public static final String AFTER_START_EVENT = "after_start";
public static final String STOP_EVENT = "stop";
public static final String BEFORE_STOP_EVENT = "before_stop";
public static final String AFTER_STOP_EVENT = "after_stop";
public static final String DESTROY_EVENT = "destroy";
// 生命周期方法
public void init() throws LifecycleException;
public void start() throws LifecycleException;
public void stop() throws LifecycleException;
public void destroy() throws LifecycleException;
// 生命周期状态
public LifecycleState getState();
// 监听器管理
public void addLifecycleListener(LifecycleListener listener);
public void removeLifecycleListener(LifecycleListener listener);
}LifecycleBase 抽象实现
LifecycleBase 提供了生命周期管理的模板方法:
public abstract class LifecycleBase implements Lifecycle {
private volatile LifecycleState state = LifecycleState.NEW;
@Override
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
initInternal(); // 调用子类实现
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}
@Override
public final synchronized void start() throws LifecycleException {
if (LifecycleState.STARTING_PREP.equals(state) ||
LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {
return;
}
if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
}
setStateInternal(LifecycleState.STARTING_PREP, null, false);
try {
startInternal(); // 调用子类实现
setStateInternal(LifecycleState.STARTED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}
// 子类需要实现的抽象方法
protected abstract void initInternal() throws LifecycleException;
protected abstract void startInternal() throws LifecycleException;
protected abstract void stopInternal() throws LifecycleException;
protected abstract void destroyInternal() throws LifecycleException;
}核心组件初始化详解
Server 初始化
StandardServer.initInternal() 方法:
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
// 注册全局字符串缓存
onameStringCache = register(new StringCache(), "type=StringCache");
// 注册 MBeanFactory
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
onameMBeanFactory = register(factory, "type=MBeanFactory");
// 初始化全局命名资源
globalNamingResources.init();
// 初始化所有 Service
for (Service service : services) {
service.init();
}
}Service 初始化
StandardService.initInternal() 方法:
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
// 初始化 Engine
if (engine != null) {
engine.init();
}
// 初始化 Executor
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
// 初始化 MapperListener
mapperListener.init();
// 初始化所有 Connector
synchronized (connectorsLock) {
for (Connector connector : connectors) {
connector.init();
}
}
}Engine 初始化
StandardEngine.initInternal() 方法:
@Override
protected void initInternal() throws LifecycleException {
// 确保存在 Realm
getRealm();
super.initInternal();
}
// ContainerBase.initInternal()
@Override
protected void initInternal() throws LifecycleException {
BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
startStopExecutor = new ThreadPoolExecutor(
getStartStopThreadsInternal(),
getStartStopThreadsInternal(),
10, TimeUnit.SECONDS,
startStopQueue,
new StartStopThreadFactory(getName() + "-startStop-"));
startStopExecutor.allowCoreThreadTimeOut(true);
super.initInternal();
}Connector 初始化
Connector.initInternal() 方法负责初始化网络连接组件:
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
// 初始化适配器
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
// 设置解析器配置
if (null == parseBodyMethodsSet) {
setParseBodyMethods(getParseBodyMethods());
}
// 初始化 ProtocolHandler
try {
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}组件启动流程
Catalina.start() 方法
初始化完成后,开始启动各组件:
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal(sm.getString("catalina.noServer"));
return;
}
long t1 = System.nanoTime();
// 启动 Server
try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}
long t2 = System.nanoTime();
if (log.isInfoEnabled()) {
log.info(sm.getString("catalina.startup",
Long.valueOf((t2 - t1) / 1000000)));
}
// 注册关闭钩子
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
// 等待关闭命令
if (await) {
await();
stop();
}
}Server 启动
StandardServer.startInternal() 方法:
@Override
protected void startInternal() throws LifecycleException {
fireLifecycleEvent(CONFIGURE_START_EVENT, null);
setState(LifecycleState.STARTING);
// 启动全局命名资源
globalNamingResources.start();
// 启动所有 Service
synchronized (servicesLock) {
for (Service service : services) {
service.start();
}
}
// 启动周期性事件
if (periodicEventDelay > 0) {
monitorFuture = getUtilityExecutor().scheduleWithFixedDelay(
new Runnable() {
@Override
public void run() {
startPeriodicLifecycleEvent();
}
}, 0, 60, TimeUnit.SECONDS);
}
}Service 启动
StandardService.startInternal() 方法:
@Override
protected void startInternal() throws LifecycleException {
if (log.isInfoEnabled()) {
log.info(sm.getString("standardService.start.name", this.name));
}
setState(LifecycleState.STARTING);
// 启动 Engine
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
// 启动 Executor
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
// 启动 MapperListener
mapperListener.start();
// 启动所有 Connector
synchronized (connectorsLock) {
for (Connector connector: connectors) {
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
}
}
}Engine 和 Host 启动
StandardEngine.startInternal() 方法:
@Override
protected synchronized void startInternal() throws LifecycleException {
// 记录启动信息
if (log.isInfoEnabled()) {
log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
}
super.startInternal();
}
// ContainerBase.startInternal()
@Override
protected synchronized void startInternal() throws LifecycleException {
// 启动 Cluster
Cluster cluster = getClusterInternal();
if (cluster instanceof Lifecycle) {
((Lifecycle) cluster).start();
}
// 启动 Realm
Realm realm = getRealmInternal();
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}
// 启动子容器(Host)
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
results.add(startStopExecutor.submit(new StartChild(child)));
}
// 等待所有子容器启动完成
MultiThrowable multiThrowable = null;
for (Future<Void> result : results) {
try {
result.get();
} catch (Throwable e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
if (multiThrowable == null) {
multiThrowable = new MultiThrowable();
}
multiThrowable.add(e);
}
}
// 启动管道
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
setState(LifecycleState.STARTING);
// 启动后台线程
threadStart();
}Context 启动(Web 应用部署)
StandardContext.startInternal() 方法负责部署 Web 应用:
@Override
protected synchronized void startInternal() throws LifecycleException {
// 发布启动事件
setConfigured(false);
boolean ok = true;
// 创 建工作目录
postWorkDirectory();
// 创建类加载器
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader();
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
// 初始化字符集映射
getCharsetMapper();
// 启动类加载器
if (loader != null && loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}
// 启动 Realm
if (realm != null && realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}
// 触发 CONFIGURE_START_EVENT
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
// 启动子容器(Wrapper)
for (Container child : findChildren()) {
if (!child.getState().isAvailable()) {
child.start();
}
}
// 启动管道
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
// 调用 ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
// 配置并启动 Filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// 加载启动时需要加载的 Servlet
if (ok) {
if (!loadOnStartup(findChildren())) {
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
// 启动后台线程
super.threadStart();
// 设置状态
if (ok) {
setState(LifecycleState.STARTING);
} else {
setState(LifecycleState.FAILED);
}
}Connector 启动
Connector.startInternal() 方法启动网络监听:
@Override
protected void startInternal() throws LifecycleException {
// 验证端口
if (getPortWithOffset() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset())));
}
setState(LifecycleState.STARTING);
try {
// 启动 ProtocolHandler
protocolHandler.start();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
}
}NIO 端点启动
NioEndpoint 启动过程
NioEndpoint.startInternal() 方法:
@Override
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
// 创建处理器缓存
if (getExecutor() == null) {
createExecutor();
}
// 初始化连接数限制
initializeConnectionLatch();
// 创建 Poller 线程
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-Poller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
// 创建 Acceptor 线程
startAcceptorThread();
}
}
protected void startAcceptorThread() {
acceptor = new Acceptor<>(this);
String threadName = getName() + "-Acceptor";
acceptor.setThreadName(threadName);
Thread t = new Thread(acceptor, threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}Acceptor 线程
Acceptor 负责接收客户端连接:
public class Acceptor<U> implements Runnable {
@Override
public void run() {
int errorDelay = 0;
while (endpoint.isRunning()) {
// 限制连接数
while (endpoint.isPaused() && endpoint.isRunning()) {
state = AcceptorState.PAUSED;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// Ignore
}
}
if (!endpoint.isRunning()) {
break;
}
state = AcceptorState.RUNNING;
try {
// 等待连接数限制
endpoint.countUpOrAwaitConnection();
// 接收连接
SocketChannel socket = null;
try {
socket = endpoint.serverSocketAccept();
} catch (Exception ioe) {
// 错误处理
endpoint.countDownConnection();
if (endpoint.isRunning()) {
errorDelay = handleExceptionWithDelay(errorDelay);
throw ioe;
} else {
break;
}
}
// 成功接收连接
errorDelay = 0;
// 配置 Socket
if (endpoint.isRunning() && !endpoint.isPaused()) {
if (!endpoint.setSocketOptions(socket)) {
endpoint.closeSocket(socket);
}
} else {
endpoint.destroySocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
}
state = AcceptorState.ENDED;
}
}Poller 线程
Poller 负责处理 I/O 事件:
public class Poller implements Runnable {
private Selector selector;
private final SynchronizedQueue<PollerEvent> events =
new SynchronizedQueue<>();
@Override
public void run() {
while (true) {
boolean hasEvents = false;
try {
if (!close) {
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
if (close) {
events();
timeout(0, false);
try {
selector.close();
} catch (IOException ioe) {
log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
}
break;
}
} catch (Throwable x) {
ExceptionUtils.handleThrowable(x);
log.error(sm.getString("endpoint.nio.selectorLoopError"), x);
continue;
}
if (keyCount == 0) {
hasEvents = (hasEvents | events());
}
// 处理就绪的 I/O 事件
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper != null) {
processKey(sk, socketWrapper);
}
iterator.remove();
}
// 处理超时
timeout(keyCount, hasEvents);
}
}
}启动优化建议
1. JVM 参数优化
在使用 Trae IDE 开发和调试 Tomcat 应用时,合理的 JVM 参数配置能显著提升启动速度:
# 优化启动速度的 JVM 参数
JAVA_OPTS="-server \
-Xms2048m -Xmx2048m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:+ParallelRefProcEnabled \
-XX:+DisableExplicitGC \
-Djava.awt.headless=true \
-Djava.security.egd=file:/dev/./urandom"2. 并行部署配置
启用并行部署可以加快多应用启动:
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true"
startStopThreads="4">
</Host>3. 扫描优化
减少不必要的 JAR 扫描:
# catalina.properties
tomcat.util.scan.StandardJarScanFilter.jarsToSkip=\
bootstrap.jar,commons-daemon.jar,tomcat-juli.jar,\
annotations-api.jar,el-api.jar,jsp-api.jar,servlet-api.jar,\
websocket-api.jar,jaspic-api.jar,catalina.jar,catalina-ant.jar4. 连接器优化
优化 Connector 配置:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
maxThreads="200"
minSpareThreads="10"
acceptCount="100"
maxConnections="10000"
compression="on"
compressionMinSize="2048"
redirectPort="8443" />故障排查技巧
1. 启动日志分析
关键日志位置:
logs/catalina.out:主要启动日志logs/catalina.{date}.log:按日期分割的日志logs/localhost.{date}.log:应用部署日志
2. 常见启动 问题
端口占用:
# 检查端口占用
lsof -i:8080
netstat -tlnp | grep 8080内存不足:
# 增加 JVM 堆内存
export JAVA_OPTS="-Xms512m -Xmx2048m"权限问题:
# 确保 Tomcat 用户有相应权限
chown -R tomcat:tomcat $CATALINA_HOME
chmod +x $CATALINA_HOME/bin/*.sh3. 调试模式启动
启用远程调试:
# 在 catalina.sh 中添加
CATALINA_OPTS="$CATALINA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"在 Trae IDE 中配置远程调试,可以方便地调试 Tomcat 启动过程。
总结
Tomcat 的启动过程是一个复杂而精密的流程,涉及脚本执行、类加载器初始化、配置文件解析、组件生命周期管理等多个环节。通过深入理解这个过程,我们可以:
- 快速定位问题:当启动失败时,能够根据日志和错误信息快速定位问题所在
- 优化启动性能:通过合理配置参数和优化部署策略,显著提升启动速度
- 定制化开发:基于 Tomcat 的生命周期机制,开发自定义组件和扩展
- 提升运维能力:更好地进行容量规划、性能调优和故障恢复
在实际开发中,结合 Trae IDE 的强大功能,如智能代码补全、实时调试、性能分析等,可以让我们更高效地开发和维护基于 Tomcat 的 Java Web 应用。无论是开发新应用还是维护现有系统,深入理解 Tomcat 的启动机制都是提升开发效率的重要基础。
(此内容由 AI 辅助生成,仅供参考)