Tomcat Servlet容器与生命周期管理面试题

1. Tomcat的容器层次结构是什么?

答案:

Tomcat采用分层的容器架构,每一层都实现了Container接口。

容器层次结构:

复制代码
Server (StandardServer)
  └── Service (StandardService)
        ├── Connector (多个)
        └── Engine (StandardEngine)
              └── Host (StandardHost - 多个)
                    └── Context (StandardContext - 多个)
                          └── Wrapper (StandardWrapper - 多个)

各层容器详解:

1. Server - 服务器实例

  • 实现类: org.apache.catalina.core.StandardServer
  • 职责:
    • 代表整个Catalina servlet容器
    • 管理全局资源(JNDI)
    • 监听shutdown端口
  • 配置:
xml 复制代码
<Server port="8005" shutdown="SHUTDOWN">
  ...
</Server>

2. Service - 服务组

  • 实现类: org.apache.catalina.core.StandardService
  • 职责:
    • 将多个Connector与一个Engine关联
    • 允许多个协议共享同一组web应用
  • 配置:
xml 复制代码
<Service name="Catalina">
  <Connector ... />
  <Engine ... />
</Service>

3. Engine - 引擎

  • 实现类: org.apache.catalina.core.StandardEngine
  • 职责:
    • 处理所有Connector的请求
    • 包含多个虚拟主机(Host)
    • 支持集群和负载均衡
  • 配置:
xml 复制代码
<Engine name="Catalina" defaultHost="localhost">
  ...
</Engine>

4. Host - 虚拟主机

  • 实现类: org.apache.catalina.core.StandardHost
  • 职责:
    • 代表一个虚拟主机
    • 管理web应用的部署
    • 支持域名别名
  • 配置:
xml 复制代码
<Host name="localhost" appBase="webapps"
      unpackWARs="true" autoDeploy="true">
  ...
</Host>

5. Context - Web应用

  • 实现类: org.apache.catalina.core.StandardContext
  • 职责:
    • 代表一个Web应用(ServletContext)
    • 管理Servlet、Filter、Listener
    • 处理会话管理
    • 管理类加载器
  • 配置:
xml 复制代码
<Context path="/myapp" docBase="myapp.war"
         reloadable="true">
  ...
</Context>

6. Wrapper - Servlet包装器

  • 实现类: org.apache.catalina.core.StandardWrapper
  • 职责:
    • 代表一个Servlet定义
    • 管理Servlet生命周期
    • 处理Servlet的初始化参数
    • 管理Servlet实例池(单线程模式)
  • 配置: 通过web.xml或注解定义

容器特性:

1. 每个容器都有:

  • Pipeline (管道)
  • Valve (阀门)
  • Lifecycle (生命周期管理)
  • Loader (类加载器 - Context级别)
  • Manager (会话管理器 - Context级别)
  • Realm (安全域)

2. 请求处理流程:

复制代码
Connector
  ↓
Engine.Pipeline → Engine.Valve
  ↓
Host.Pipeline → Host.Valve
  ↓
Context.Pipeline → Context.Valve
  ↓
Wrapper.Pipeline → Wrapper.Valve
  ↓
Servlet.service()

2. Tomcat的生命周期管理机制是什么?

答案:

Tomcat使用统一的生命周期管理机制,所有主要组件都实现Lifecycle接口。

Lifecycle接口

位置: org.apache.catalina.Lifecycle

核心方法:

java 复制代码
public interface Lifecycle {
    void init() throws LifecycleException;
    void start() throws LifecycleException;
    void stop() throws LifecycleException;
    void destroy() throws LifecycleException;

    void addLifecycleListener(LifecycleListener listener);
    void removeLifecycleListener(LifecycleListener listener);
}

生命周期状态:

复制代码
NEW (新建)
  ↓ init()
INITIALIZING (初始化中)
  ↓
INITIALIZED (已初始化)
  ↓ start()
STARTING_PREP (启动准备)
  ↓
STARTING (启动中)
  ↓
STARTED (已启动)
  ↓ stop()
STOPPING_PREP (停止准备)
  ↓
STOPPING (停止中)
  ↓
STOPPED (已停止)
  ↓ destroy()
DESTROYING (销毁中)
  ↓
DESTROYED (已销毁)

生命周期事件:

初始化阶段:

  • BEFORE_INIT_EVENT - 初始化前
  • AFTER_INIT_EVENT - 初始化后

启动阶段:

  • BEFORE_START_EVENT - 启动前
  • START_EVENT - 启动时
  • AFTER_START_EVENT - 启动后

停止阶段:

  • BEFORE_STOP_EVENT - 停止前
  • STOP_EVENT - 停止时
  • AFTER_STOP_EVENT - 停止后

销毁阶段:

  • BEFORE_DESTROY_EVENT - 销毁前
  • AFTER_DESTROY_EVENT - 销毁后

LifecycleBase实现:

位置: org.apache.catalina.util.LifecycleBase

模板方法模式:

java 复制代码
public abstract class LifecycleBase implements Lifecycle {

    @Override
    public final synchronized void init() throws LifecycleException {
        // 状态检查
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }

        // 触发BEFORE_INIT事件
        setStateInternal(LifecycleState.INITIALIZING, null, false);

        // 调用子类实现
        initInternal();

        // 触发AFTER_INIT事件
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    }

    protected abstract void initInternal() throws LifecycleException;
    protected abstract void startInternal() throws LifecycleException;
    protected abstract void stopInternal() throws LifecycleException;
    protected abstract void destroyInternal() throws LifecycleException;
}

组件启动顺序:

复制代码
1. Server.init()
   └── Service.init()
         ├── Engine.init()
         │     └── Host.init()
         │           └── Context.init()
         │                 └── Wrapper.init()
         └── Connector.init()

2. Server.start()
   └── Service.start()
         ├── Engine.start()
         │     └── Host.start()
         │           └── Context.start()
         │                 └── Wrapper.start()
         └── Connector.start()

生命周期监听器:

自定义监听器:

java 复制代码
public class MyLifecycleListener implements LifecycleListener {
    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
            System.out.println("Component started: " + event.getLifecycle());
        }
    }
}

配置监听器:

xml 复制代码
<Server>
  <Listener className="com.example.MyLifecycleListener" />
</Server>

3. Servlet的生命周期在Tomcat中是如何管理的?

答案:

Servlet的生命周期由Wrapper组件管理。

Servlet生命周期阶段:

1. 加载和实例化

java 复制代码
// StandardWrapper.loadServlet()
public synchronized Servlet loadServlet() throws ServletException {
    // 使用WebappClassLoader加载Servlet类
    Class<?> clazz = classLoader.loadClass(servletClass);

    // 实例化Servlet
    Servlet servlet = (Servlet) clazz.newInstance();

    return servlet;
}

触发时机:

  • 第一次请求到达时(懒加载)
  • 配置了load-on-startup时在启动时加载

2. 初始化 (init)

java 复制代码
// StandardWrapper.initServlet()
private synchronized void initServlet(Servlet servlet) throws ServletException {
    // 创建ServletConfig
    ServletConfig config = new StandardWrapperFacade(this);

    // 调用Servlet.init()
    servlet.init(config);

    // 标记为已初始化
    singleThreadModel = servlet instanceof SingleThreadModel;
}

ServletConfig提供:

  • Servlet名称
  • ServletContext引用
  • 初始化参数

3. 服务 (service)

java 复制代码
// StandardWrapper.allocate()
public Servlet allocate() throws ServletException {
    // 单线程模式:从池中获取实例
    if (singleThreadModel) {
        return pool.pop();
    }

    // 多线程模式:返回单例
    return instance;
}

// StandardWrapperValve.invoke()
public void invoke(Request request, Response response) {
    // 分配Servlet实例
    Servlet servlet = wrapper.allocate();

    // 创建请求和响应门面
    ServletRequest req = request.getRequest();
    ServletResponse res = response.getResponse();

    // 调用Servlet.service()
    servlet.service(req, res);

    // 释放Servlet实例
    wrapper.deallocate(servlet);
}

4. 销毁 (destroy)

java 复制代码
// StandardWrapper.unload()
public synchronized void unload() throws ServletException {
    // 调用Servlet.destroy()
    if (instance != null) {
        instance.destroy();
        instance = null;
    }

    // 清理单线程模式的实例池
    if (singleThreadModel && pool != null) {
        while (!pool.isEmpty()) {
            pool.pop().destroy();
        }
    }
}

触发时机:

  • Context停止或重新加载
  • Tomcat关闭
  • Servlet被替换

load-on-startup配置:

web.xml:

xml 复制代码
<servlet>
    <servlet-name>InitServlet</servlet-name>
    <servlet-class>com.example.InitServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

注解:

java 复制代码
@WebServlet(name = "InitServlet",
            urlPatterns = "/init",
            loadOnStartup = 1)
public class InitServlet extends HttpServlet {
    // ...
}

加载顺序:

  • 值越小越先加载
  • 负数表示懒加载
  • 相同值的加载顺序不确定

SingleThreadModel (已废弃):

java 复制代码
public class OldServlet extends HttpServlet implements SingleThreadModel {
    // Tomcat会为每个请求创建新实例或使用实例池
}

问题:

  • 性能开销大
  • 无法保证线程安全(静态变量、外部资源)
  • Servlet 2.4已废弃

异步Servlet:

Servlet 3.0+支持:

java 复制代码
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        AsyncContext asyncContext = req.startAsync();
        asyncContext.start(() -> {
            // 异步处理
            asyncContext.complete();
        });
    }
}

4. Tomcat的Pipeline和Valve机制是什么?

答案:

Pipeline-Valve是Tomcat实现请求处理链的核心机制,类似于责任链模式。

架构设计:

Pipeline接口:

java 复制代码
public interface Pipeline {
    Valve getBasic();
    void setBasic(Valve valve);
    void addValve(Valve valve);
    Valve[] getValves();
    void removeValve(Valve valve);
    Valve getFirst();
}

Valve接口:

java 复制代码
public interface Valve {
    Valve getNext();
    void setNext(Valve next);
    void invoke(Request request, Response response)
        throws IOException, ServletException;
}

每个容器的Pipeline:

复制代码
Engine Pipeline:
  StandardEngineValve (Basic Valve)

Host Pipeline:
  ErrorReportValve → StandardHostValve (Basic Valve)

Context Pipeline:
  NonLoginAuthenticator → StandardContextValve (Basic Valve)

Wrapper Pipeline:
  StandardWrapperValve (Basic Valve)

请求处理流程:

复制代码
1. Connector接收请求
   ↓
2. Engine.Pipeline.first.invoke()
   ↓
3. [自定义Valve] → StandardEngineValve
   ↓ (选择Host)
4. Host.Pipeline.first.invoke()
   ↓
5. [ErrorReportValve] → StandardHostValve
   ↓ (选择Context)
6. Context.Pipeline.first.invoke()
   ↓
7. [AuthenticatorValve] → StandardContextValve
   ↓ (选择Wrapper)
8. Wrapper.Pipeline.first.invoke()
   ↓
9. StandardWrapperValve
   ↓ (调用Servlet)
10. Servlet.service()

StandardEngineValve:

位置: org.apache.catalina.core.StandardEngineValve

java 复制代码
public void invoke(Request request, Response response) {
    // 获取Host
    Host host = request.getHost();
    if (host == null) {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
        return;
    }

    // 调用Host的Pipeline
    host.getPipeline().getFirst().invoke(request, response);
}

StandardHostValve:

位置: org.apache.catalina.core.StandardHostValve

java 复制代码
public void invoke(Request request, Response response) {
    // 获取Context
    Context context = request.getContext();
    if (context == null) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    // 调用Context的Pipeline
    context.getPipeline().getFirst().invoke(request, response);
}

StandardContextValve:

位置: org.apache.catalina.core.StandardContextValve

java 复制代码
public void invoke(Request request, Response response) {
    // 获取Wrapper
    Wrapper wrapper = request.getWrapper();
    if (wrapper == null) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    // 调用Wrapper的Pipeline
    wrapper.getPipeline().getFirst().invoke(request, response);
}

StandardWrapperValve:

位置: org.apache.catalina.core.StandardWrapperValve

java 复制代码
public void invoke(Request request, Response response) {
    // 分配Servlet实例
    Servlet servlet = wrapper.allocate();

    // 创建Filter链
    ApplicationFilterChain filterChain =
        ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

    // 执行Filter链和Servlet
    filterChain.doFilter(request.getRequest(), response.getResponse());

    // 释放Servlet实例
    wrapper.deallocate(servlet);
}

自定义Valve:

实现Valve:

java 复制代码
public class CustomValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response)
            throws IOException, ServletException {
        // 前置处理
        long startTime = System.currentTimeMillis();

        // 调用下一个Valve
        getNext().invoke(request, response);

        // 后置处理
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Request took: " + duration + "ms");
    }
}

配置Valve:

xml 复制代码
<Host name="localhost">
    <Valve className="com.example.CustomValve" />
</Host>

常用内置Valve:

1. AccessLogValve - 访问日志

xml 复制代码
<Valve className="org.apache.catalina.valves.AccessLogValve"
       directory="logs"
       prefix="localhost_access_log"
       suffix=".txt"
       pattern="%h %l %u %t &quot;%r&quot; %s %b" />

2. RemoteAddrValve - IP过滤

xml 复制代码
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
       allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />

3. ErrorReportValve - 错误报告

xml 复制代码
<Valve className="org.apache.catalina.valves.ErrorReportValve"
       showReport="false"
       showServerInfo="false" />

4. RewriteValve - URL重写

xml 复制代码
<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />

5. Tomcat的类加载机制是什么?

答案:

Tomcat使用自定义的类加载器层次结构,实现应用隔离和热部署。

类加载器层次:

复制代码
Bootstrap ClassLoader (JVM)
  ↓
System ClassLoader (JVM)
  ↓
Common ClassLoader ($CATALINA_HOME/lib)
  ↓
  ├── Server ClassLoader (server classes - 已移除)
  └── Shared ClassLoader (shared classes - 可选)
        ↓
        WebappClassLoader (/WEB-INF/classes, /WEB-INF/lib)
          ↓
          Jsp ClassLoader (JSP编译后的类)

WebappClassLoader:

位置: org.apache.catalina.loader.WebappClassLoaderBase

特点:

  • 每个Web应用有独立的类加载器
  • 实现应用隔离
  • 支持热部署

加载顺序 (默认):

  1. Bootstrap和System类加载器 (Java核心类)
  2. /WEB-INF/classes (应用类)
  3. /WEB-INF/lib/*.jar (应用库)
  4. Common ClassLoader (Tomcat共享库)

委托模式:

默认情况下,WebappClassLoader打破了双亲委派模型:

java 复制代码
public Class<?> loadClass(String name) throws ClassNotFoundException {
    // 1. 检查是否已加载
    Class<?> clazz = findLoadedClass(name);
    if (clazz != null) {
        return clazz;
    }

    // 2. 系统类委托给父加载器
    if (name.startsWith("java.")) {
        return parent.loadClass(name);
    }

    // 3. 尝试自己加载 (打破双亲委派)
    try {
        clazz = findClass(name);
        if (clazz != null) {
            return clazz;
        }
    } catch (ClassNotFoundException e) {
        // 继续
    }

    // 4. 委托给父加载器
    return parent.loadClass(name);
}

配置委托模式:

xml 复制代码
<Context path="/myapp" docBase="myapp.war">
    <Loader delegate="true"/>
</Context>
  • delegate="false" (默认): 先自己加载,再委托父加载器
  • delegate="true": 先委托父加载器,再自己加载(标准双亲委派)

类加载路径:

Common ClassLoader加载:

  • $CATALINA_HOME/lib
  • $CATALINA_BASE/lib

WebappClassLoader加载:

  • /WEB-INF/classes
  • /WEB-INF/lib/*.jar

热部署实现:

Context配置:

xml 复制代码
<Context path="/myapp" docBase="myapp.war" reloadable="true">
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/classes</WatchedResource>
</Context>

工作原理:

  1. ContainerBackgroundProcessor 定期检查
  2. 检测到类文件变化
  3. 调用 Context.reload()
  4. 停止Context
  5. 销毁旧的WebappClassLoader
  6. 创建新的WebappClassLoader
  7. 重新加载所有类
  8. 启动Context

代码实现:

java 复制代码
// StandardContext.backgroundProcess()
public void backgroundProcess() {
    if (reloadable && modified()) {
        reload();
    }
}

// StandardContext.reload()
public synchronized void reload() {
    // 停止Context
    stop();

    // 启动Context (会创建新的ClassLoader)
    start();
}

ParallelWebappClassLoader:

位置: org.apache.catalina.loader.ParallelWebappClassLoader

特点:

  • 支持并行类加载 (Java 7+)
  • 提高多线程环境下的加载性能
  • 避免死锁

配置:

xml 复制代码
<Context>
    <Loader loaderClass="org.apache.catalina.loader.ParallelWebappClassLoader"/>
</Context>

类加载问题排查:

1. ClassNotFoundException

  • 检查类是否在正确的路径
  • 检查JAR文件是否损坏
  • 检查类加载器委托配置

2. NoClassDefFoundError

  • 类的依赖类找不到
  • 静态初始化失败

3. ClassCastException

  • 同一个类被不同类加载器加载
  • 检查是否有重复的JAR

4. LinkageError

  • 类被多次加载
  • 检查Common和WebApp路径中的重复JAR

6. Tomcat的会话管理机制是什么?

答案:

Tomcat通过Manager组件管理HTTP会话。

Manager接口:

位置: org.apache.catalina.Manager

核心方法:

java 复制代码
public interface Manager {
    Session createSession(String sessionId);
    Session findSession(String id) throws IOException;
    void remove(Session session);
    void add(Session session);
    Session[] findSessions();
    void load() throws IOException;
    void unload() throws IOException;
}

Manager实现类:

1. StandardManager (默认)

特点:

  • 会话存储在内存中
  • 支持会话持久化到文件
  • 单机部署

配置:

xml 复制代码
<Context>
    <Manager className="org.apache.catalina.session.StandardManager"
             maxActiveSessions="1000"
             sessionIdLength="16" />
</Context>

持久化:

  • Tomcat正常关闭时,会话序列化到 SESSIONS.ser
  • 启动时从文件恢复会话

2. PersistentManager

特点:

  • 支持会话持久化到Store
  • 支持会话钝化(Passivation)
  • 节省内存

配置:

xml 复制代码
<Context>
    <Manager className="org.apache.catalina.session.PersistentManager"
             maxActiveSessions="1000"
             minIdleSwap="5"
             maxIdleSwap="10"
             maxIdleBackup="2">
        <Store className="org.apache.catalina.session.FileStore"
               directory="sessions"/>
    </Manager>
</Context>

参数说明:

  • minIdleSwap: 会话空闲多久后钝化到Store
  • maxIdleSwap: 会话空闲多久后必须钝化
  • maxIdleBackup: 会话空闲多久后备份到Store

3. DeltaManager (集群)

特点:

  • 会话复制到集群所有节点
  • 适合小集群(< 4节点)
  • 全量复制

配置:

xml 复制代码
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
    <Manager className="org.apache.catalina.ha.session.DeltaManager"
             expireSessionsOnShutdown="false"
             notifyListenersOnReplication="true"/>
</Cluster>

4. BackupManager (集群)

特点:

  • 会话只复制到一个备份节点
  • 适合大集群
  • 节省网络带宽

配置:

xml 复制代码
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
    <Manager className="org.apache.catalina.ha.session.BackupManager"
             mapSendOptions="6"/>
</Cluster>

Session实现:

StandardSession:

java 复制代码
public class StandardSession implements HttpSession, Session, Serializable {
    protected String id;                    // 会话ID
    protected long creationTime;            // 创建时间
    protected long lastAccessedTime;        // 最后访问时间
    protected int maxInactiveInterval;      // 最大不活动时间
    protected Map<String, Object> attributes; // 会话属性
    protected boolean isValid;              // 是否有效
    protected Manager manager;              // 所属Manager
}

会话ID生成:

SessionIdGenerator:

java 复制代码
// org.apache.catalina.util.StandardSessionIdGenerator
public String generateSessionId() {
    byte[] random = new byte[16];
    secureRandom.nextBytes(random);
    return toHexString(random);
}

配置:

xml 复制代码
<Manager sessionIdLength="32">
    <SessionIdGenerator className="org.apache.catalina.util.StandardSessionIdGenerator"
                        sessionIdLength="32"/>
</Manager>

会话超时:

配置超时时间:

web.xml:

xml 复制代码
<session-config>
    <session-timeout>30</session-timeout> <!-- 分钟 -->
</session-config>

Context.xml:

xml 复制代码
<Context sessionTimeout="30"/>

程序设置:

java 复制代码
session.setMaxInactiveInterval(1800); // 秒

超时检查:

java 复制代码
// StandardManager.backgroundProcess()
public void backgroundProcess() {
    Session[] sessions = findSessions();
    for (Session session : sessions) {
        if (session.isValid() && session.isExpired()) {
            session.expire();
        }
    }
}

会话监听器:

HttpSessionListener:

java 复制代码
@WebListener
public class SessionListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("Session created: " + se.getSession().getId());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("Session destroyed: " + se.getSession().getId());
    }
}

HttpSessionAttributeListener:

java 复制代码
@WebListener
public class SessionAttributeListener implements HttpSessionAttributeListener {
    @Override
    public void attributeAdded(HttpSessionBindingEvent se) {
        System.out.println("Attribute added: " + se.getName());
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent se) {
        System.out.println("Attribute removed: " + se.getName());
    }

    @Override
    public void attributeReplaced(HttpSessionBindingEvent se) {
        System.out.println("Attribute replaced: " + se.getName());
    }
}

Cookie配置:

Context.xml:

xml 复制代码
<Context>
    <CookieProcessor className="org.apache.tomcat.util.http.Rfc6265CookieProcessor"
                     sameSiteCookies="strict"/>
</Context>

web.xml:

xml 复制代码
<session-config>
    <cookie-config>
        <name>MYSESSIONID</name>
        <domain>.example.com</domain>
        <path>/</path>
        <http-only>true</http-only>
        <secure>true</secure>
        <max-age>3600</max-age>
    </cookie-config>
</session-config>
相关推荐
indexsunny1 小时前
互联网大厂Java求职面试实战:Spring Boot微服务与Kafka消息队列场景解析
java·spring boot·面试·kafka·microservices·interview·distributed systems
qq_12498707531 小时前
基于Spring Boot的心理咨询预约微信小程序(源码+论文+部署+安装)
java·spring boot·后端·spring·微信小程序·小程序·毕业设计
彭于晏Yan2 小时前
SpringBoot集成Druid连接多个数据源
java·spring boot·后端
拽着尾巴的鱼儿2 小时前
fixedBug:Web Requeset Get请求URLEncoder 编码
java
lkbhua莱克瓦242 小时前
Apache Maven全面解析
java·数据库·笔记·maven·apache
爱编码的傅同学2 小时前
【线程的同步与互斥】初识互斥量与锁
android·java·开发语言
ChoSeitaku2 小时前
28.C++进阶:map和set封装|insert|迭代器|[]
java·c++·算法
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 收藏功能实现
android·java·开发语言·javascript·python·flutter·游戏
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 个人中心实现
android·java·javascript·python·flutter·游戏