ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot核心面试知识点全解析:自动装配原理、CGLIB代理与配置体系

SpringBoot核心面试知识点全解析:自动装配原理、CGLIB代理与配置体系 进入金三银四也好日常骑驴找马也罢SpringBoot 相关的问题几乎是面试必考项。我这些年面过不少候选人也在技术社区里围观过大量面试复盘感触最深的一点是很多人框架用得挺熟CRUD 写得飞起被问到 SpringBoot 自动装配的原理、为什么默认走 CGLIB 代理、配置文件优先级这类问题时反而聊不到点子上。这篇整理不是写给零基础读者怎么建项目的也不是给架构师讲高并发治理的而是把我认为面试前最值得过一遍的 SpringBoot 核心知识点按主题拆开。每个主题都会讲清楚“是什么”“为什么”“面试官想听到什么”同时附上一些实际项目里踩过的坑。你可以把它当成面试前的速查笔记也可以当成复习提纲哪块不熟就翻哪块。1. 自动装配原理SpringBoot 最核心的面试考点1.1 三个注解融为一体自动装配几乎是所有 SpringBoot 面试的开场白。面试官通常会先问“你说说 SpringBoot 为什么能自动配置”如果你直接背诵“因为有 EnableAutoConfiguration”那只能算及格离加分还差得远。首先得把启动类上的 SpringBootApplication 拆开。它本质上是三个注解的组合SpringBootConfiguration、EnableAutoConfiguration、ComponentScan。其中 SpringBootConfiguration 底层就是 Configuration表示这是一个配置类ComponentScan 负责扫描主类所在包及其子包下的组件真正承担自动装配核心逻辑的是 EnableAutoConfiguration。这里有个容易被忽略的细节为什么规范要求启动类放在根包下就是因为 ComponentScan 默认扫描的范围是启动类所在包一旦把启动类放到子包里扫描范围就会受限一些 Bean 会找不到。面试官如果顺口问一句“你遇到过 ComponentScan 失效的情况吗”往往是这个原因。1.2 自动装配的完整链路EnableAutoConfiguration 本身也是一个组合注解核心是导入了 AutoConfigurationImportSelector。这个类通过 getAutoConfigurationEntry 方法去读取候选配置清单然后再做过滤、去重、排除最终返回真正需要导入的配置类。早期版本里候选配置清单写在 META-INF/spring.factories 文件里键是 EnableAutoConfiguration。从 SpringBoot 2.7 开始官方引入了新的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件逐步替代 spring.factories 中的自动配置条目。如果你在维护老项目两个入口都要能认出来。关键点在于被列进 imports 文件只是“候选”最终是否生效还取决于每个配置类上的条件注解。这就是自动装配最常见的误区——候选人以为“配置类加载了就等于装配了”实际上配置类会被加载但内部的 Bean 方法必须通过条件判断才会真正执行。拿 RedisAutoConfiguration 举例。它上面标了一堆条件注解ConditionalOnClass(RedisOperations.class)表示 classpath 下必须有 RedisOperations 这个类ConditionalOnMissingBean 则表示容器里没有 RedisTemplate 等关键 Bean 时才执行装配。所以自动装配是“候选清单加条件判断”双重过滤后的结果。这个逻辑在面试时讲清楚基本就能和大多数候选人拉开差距。1.3 自定义自动配置与条件装配面试进阶时面试官可能让你现场说说“怎么自己写一个自动配置”。这个不能只停留在概念上得能说出关键步骤第一步写一个配置类在上面标注 Configuration 和一组条件注解。第二步把类路径写入 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。第三步提供一个带前缀的属性类比如 ConfigurationProperties(prefix my.demo)让使用方能在 application.yml 里配置参数。条件注解里最常用的是 ConditionalOnMissingBean、ConditionalOnProperty、ConditionalOnClass 这组。需要注意 ConditionalOnMissingBean 的语义它判断的是“当前容器里是否缺少某个类型的 Bean”而不是“整个 JVM 里有没有这个类”。很多人写 starter 时容易在这里翻车比如自定义 Starter 里默认注册了一个 ObjectMapper 的定制 Bean结果项目的其他模块也注册了导致默认 Bean 被跳过配置不生效。条件注解的评估时机也值得记一下它发生在容器刷新阶段并且与 BeanDefinition 的解析顺序有关。这也是为什么有时候你会看到“某个配置类明明符合条件却没生效”的玄学问题排查时先看是否被自定义 Bean 抢先注册再看 ConditionalOnProperty 的 matchIfMissing 属性配置是否合理。提示面试被问到自动装配原理时按“注解组合 - 配置类候选清单 - 条件过滤 - Bean 注册”这条链路展开比单纯背结论要完整得多。2. Starter 机制从用到写的理解路径2.1 Starter 为什么这么香自动装配解决的是“怎么配”的问题而 Starter 解决的是“依赖怎么带”的问题。两者合在一起才构成 SpringBoot 开箱即用的体验。一个 Starter 的本质是两样东西的打包依赖描述和自动配置类。比如 spring-boot-starter-web 引入了 spring-web、spring-webmvc、内嵌 Tomcat 等依赖同时因为包含 spring-boot-autoconfigure 的传递和 spring.factories/imports 机制web 相关自动配置才能被扫描到。我经常用“买路由器”来打比方。传统 SSM 整合是给你一堆零件要自己拧螺丝、接网线、装驱动SpringBoot Starter 是一台免安装的智能路由通电自动识别网络你只需要设置 WiFi 密码改几个配置项。这也是为什么面试官喜欢问“为什么用 SpringBoot 开发效率高”答案的本质不是 IDEA 快而是 Starter 机制把“依赖管理”和“自动配置”这两件麻烦事标准化了。2.2 命名规范与版本管理命名这件事看起来不起眼面试和实际项目里都有讲究。官方 Starter 命名是 spring-boot-starter-xxx比如 spring-boot-starter-data-redis。第三方自定义 Starter 如果要在 SpringBoot 项目里直接引用通常有两种命名风格一是 spring-boot-starter-xxx二是 xxx-spring-boot-starter区别在于前者容易被官方依赖分类混淆后者能明确表达“这是一款针对 SpringBoot 的第三方 Starter”。版本管理上有一个实际踩过很多次的坑使用了非官方 Starter 时一定要关注它编译对应的 SpringBoot 版本。常见问题是项目里引了某个 mybatis-spring-boot-starter 的旧版本它基于 SpringBoot 2.x 编写结果直接放进 SpringBoot 3.x 项目里启动时各种 NoSuchMethodError。所以引入第三方 Starter 前先看一眼它父依赖的 SpringBoot 版本比报错后再去搜索要省力得多。2.3 动手写一个自定义 Starter写自定义 Starter 并不神秘核心就是把逻辑下沉到一个独立模块让其他服务通过依赖引入后自动生效。项目结构通常分为 autoconfigure 模块和 starter 模块。autoconfigure 模块存放自动配置类和属性类starter 模块只做一件事依赖 autoconfigure 模块同时把自己暴露给使用方。这种拆分是为了更精细地控制依赖范围。配置类大致长这样AutoConfiguration ConditionalOnClass(MyService.class) EnableConfigurationProperties(MyProperties.class) public class MyAutoConfiguration { Bean ConditionalOnMissingBean public MyService myService(MyProperties properties) { return new MyService(properties.getPrefix()); } }然后在 resources/META-INF 下新增文件内容按行写自动配置类的全限定名。如果是 SpringBoot 2.7 之前的项目则要写入 META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.autoconfigure.MyAutoConfiguration项目里用的时候除了引入依赖还要确保 spring-boot-autoconfigure 在 classpath 中否则 AutoConfiguration 注解可能识别不到。贡献者普遍容易漏掉这一步面试手写时特意提一嘴会显得更有经验。提示自定义 Starter 的自动配置类不需要手动 ComponentScan 扫描SpringBoot 是通过 imports 文件加载的。这也是自动配置类有时候“不被项目扫描也能生效”的根本原因。3. 内嵌容器与可执行 Jar为什么 java -jar 就能跑 Web 应用3.1 可执行 Jar 的结构面试中有一个高频连环问SpringBoot 打成 Jar 包后为什么能直接 java -jar 运行普通 Java Jar 不是不能内置依赖吗答案在于 SpringBoot 的可执行 Jar 采用了特殊的嵌套结构。它会生成一个 fat jar内部包含 BOOT-INF/classes、BOOT-INF/lib、META-INF 等目录。页面启动时由 JarLauncher 负责加载嵌套的依赖 Jar而不是依赖系统 ClassLoader 直接找到所有类。具体启动链路是main 方法所在的 JarLauncher 创建一个全新的类加载器把 BOOT-INF/lib 下的所有 Jar 包装成 URL再通过反射调用我们自己写的启动类的 main 方法。这也是为什么 SpringBoot 可执行 Jar 里的类路径和普通 Jar 不同外部系统想直接拿到其中的类比较困难反编译时看到的结构也和普通项目源码不一致。面试时如果讨论“怎么将 SpringBoot Jar 反编译成项目”本质上就是要把嵌套结构还原成普通 Maven 工程解压出 BOOT-INF/classes 下的 class 文件用反编译工具转成 Java 源码再把 BOOT-INF/lib 下的依赖按 pom 坐标整理。思路不难真正耗时间的是处理资源文件、配置文件以及依赖版本的还原。3.2 容器替换与核心调优参数默认内嵌容器是 Tomcat这并不代表只能用 Tomcat。SpringBoot 支持通过引入 spring-boot-starter-jetty 或 spring-boot-starter-undertow 替换默认容器操作很简单把 web starter 里的 Tomcat 排除再替换成目标容器即可。换成 Undertow 的好处是 IO 并发场景下内存占用低我实测过高并发接口在同样配置下 Undertow 的 GC 压力比 Tomcat 小一些。但换容器不是银弹很多项目用 Tomcat 默认设置也能扛住业务量真正影响性能的是线程池、连接超时、AcceptCount 这几个参数。server: port: 8080 tomcat: max-threads: 200 min-spare-threads: 20 accept-count: 100 connection-timeout: 5000这里要注意 max-threads 不是越大越好线程太多会导致频繁上下文切换接口响应时间反而上升。通常先压测拿到基准数据再逐步调整 max-threads 和 accept-count 的组合才是规范的调优方式。3.3 外置容器部署与踩坑老项目里还有一类需求打成 war 包部署到外部 Tomcat。做法是将打包方式改为 war然后继承 SpringBootServletInitializer 并重写 configure 方法。这个考点不算难但踩坑点很典型。容易出问题的是 JSP 支持和静态资源路径。SpringBoot 默认不以 war 包里的 JSP 为主要视图需要额外引入 tomcat-embed-jasper并且 JSP 只建议放在 src/main/webapp 下。另一个坑是下划线、中划线路径在外部容器和内嵌容器下解析存在差异部署环境不同可能导致 404。所以做外置部署前一定要在目标容器版本上做一次完整回归。提示SpringBoot 3.x 部分内嵌容器的默认版本和外部 Tomcat 版本差异较大war 包部署时优先看外部 Tomcat 主版本是否满足 Servlet API 要求否则会出现 NoClassDefFoundError。4. 配置体系优先级、YAML 与多环境4.1 配置优先级与“不生效”之谜有句话说“SpringBoot 项目百分之八十的疑难杂症都出在配置上”这话不算夸张。配置优先级是其中最容易踩雷的。配置来源从高到低大致是命令行参数 Java 系统属性 操作系统环境变量 application-{profile}.yml application.yml 默认配置。很多人改了 application.yml 却不生效第一反应是“配置写错了”其实大概率是当前环境里有更高优先级的配置源覆盖了它。比如 IDE 里设置了环境变量或者启动时带了 --server.port8081这类配置就会直接压制 yml 文件里的端口配置。排查“配置不生效”时我会先确认配置来源再用 Actuator 的 /configprops 或 /env 接口查看实际生效值。这个动作比反复重启服务更高效也值得在项目里养成习惯。4.2 YAML 写法、Value 与 ConfigurationPropertiesYAML 和 properties 两种格式各有偏好。properties 简单直接适合少量配置YAML 缩进表达结构适合多级配置。但 YAML 有一个容易翻车的点数组和对象缩进不对或行内写了 Tab 字符启动时直接解析失败。建议所有 YAML 文件统一用空格缩进不要出现 Tab。从代码取配置有三种常见姿势Value 注入单个值、Environment 读取、ConfigurationProperties 绑定属性类。面试常问“Value 和 ConfigurationProperties 有什么区别”核心差别在于Value 支持 SpEL可以写复杂表达式但只能绑定简单类型ConfigurationProperties 支持复杂对象、集合、Map 的绑定且会做松散绑定比如 prefix.user-name 可以映射到 userName。实际项目中如果配置项比较多且成组强烈建议用 ConfigurationProperties 建一个属性类可读性和后续扩展性都更好。用 Value 散落在各个类里后期重构关联成本很高。4.3 Profile 多环境、随机端口与配置安全多环境管理在面试中属于送分题但很多人答不出“优先级叠加”的细节。spring.profiles.active 指定生效的 profile本质上是在 application.yml 基础之上叠加加载 application-{profile}.ymlprofile 文件优先级更高。随机端口这个热搜词也经常出现。SpringBoot 支持在配置里使用 ${random.int[1024,65535]}、${random.uuid} 等占位符。常见用法是测试环境不想冲突时把端口配置为随机server: port: ${random.int[8000,9000]}但要注意随机端口只在该应用启动时生成一次存在 Environment 里。如果你用固定端口做服务发现注册随机端口可能造成注册中心拿到的是 0 或错误端口生产环境要慎用。配置安全上常见做法是用 Jasypt 对敏感配置进行加密。引入 jasypt-spring-boot-starter 后配置里就能写 ENC(加密串)启动时用密钥解密。这种方案简单有效但密钥一定要放到启动参数或环境变量里不能直接写进配置文件否则加密就变成心理安慰了。提示SpringBoot 的配置加载顺序可以通过 spring.config.additional-location 增加外部配置目录这在容器化部署时代尤其好用把部分敏感配置放挂载目录和代码镜像解耦。5. SpringBoot 默认 CGLIB 代理与 AOP5.1 两种动态代理的区别SpringBoot 2.0 之后代理机制默认走 CGLIB这是一个经典面试考点。很多人只记得“默认 CGLIB”但说不出为什么。JDK 动态代理要求目标类必须实现接口通过 Proxy.newProxyInstance 生成一个实现同接口的代理类CGLIB 则直接继承目标类通过生成子类覆盖需要拦截的方法来实现。所以使用 CGLIB 的隐藏要求是目标类不能被 final 修饰目标方法不能是 final 或 private否则无法被继承和覆盖。SpringBoot 默认使用 CGLIB 是综合权衡的结果。一方面很多业务类没有接口JDK 代理只能退而求其次不代理另一方面 CGLIB 能拦截非接口方法配置更统一。代价是引入了 CGLIB 依赖并且 final 类、final 方法会出现代理失效的坑。5.2 代理失效的经典场景这块能展开的内容太多了面试官尤其喜欢让候选人现场判断“这个 Transactional 到底有没有生效”。最常见的场景是同类内调用。A 方法里直接调用同类 B 方法B 上有 Transactional但 A 调用的是 this.B()没有经过代理对象事务拦截器完全不参与所以事务不生效。解决办法有几种注入自身代理对象、把 B 方法抽取到另一个 Bean、或者在配置里开启 exposeProxy 后用 AopContext.currentProxy() 获取代理对象。第二种常见场景是方法本身的问题。private、final、static 方法上标 Transactional 不会生效。原因是 CGLIB 无法覆盖 private/final 方法代理逻辑进不去。还有一种是异常被吞了事务方法内 catch 住异常没抛出回滚自然不触发。第三种场景是代理失效导致的安全问题。如果你的类被 final 修饰SpringBoot 启动阶段就可能直接报错或静默降级排查时看到 “final” 关键字要第一时间想到代理机制。5.3 面试回答思路被问到“SpringBoot 为什么默认 CGLIB”打分的点在“有没有理解代理模式的应用背景”。一个不错的回答思路是先点明 Spring AOP 底层两种实现方式再说 SpringBoot 2.0 后将 proxyTargetClass 默认值从 false 改为 true因此默认 CGLIB。然后补充 CGLIB 原理和约束最后提一句“如果业务确实基于接口设计可以通过配置切回 JDK 动态代理”。这样既有结论又有原理还有工程意识面评通常不错。提示写切面时建议对接口方法使用 Before/After 注解而不是直接修改目标方法签名。否则用 CGLIB 子类代理时一旦目标方法为 final切面就可能静默失效。6. 数据访问层实战与面试要点6.1 整合 MyBatis 的完整配置SpringBoot 整合 MyBatis 是后端开发者的基础功面试时一般会问 Mapper 接口是怎么被扫描并注册成 Bean 的。最简单的方式是引入 mybatis-spring-boot-starter然后在配置类上标注 MapperScan或在每个 Mapper 接口上标 Mapper。注意 MapperScan 和 Mapper 二者选其一即可不必同时加。扫描到 Mapper 接口后MyBatis 会通过 MapperFactoryBean 注册为代理 Bean这个代理最终绑定到 SqlSessionTemplate。实际项目里我更推荐 MapperScan 加配置类理由是可以集中指定 sqlSessionTemplateRef多个数据源时也能精确绑定。另外一个容易被忽略的配置是驼峰映射mybatis: configuration: map-underscore-to-camel-case: true如果不开启这个开关数据库 user_name 字段不会自动映射到 Java 的 userName 属性结果对象里对应字段全是 null。这个现象在项目里排查过很多次十有八九是没开驼峰映射。6.2 事务管理与传播行为Transactional 是面试必背的一块。除了上一章讲的代理失效问题还需要掌握事务的传播级别和隔离级别面试官一般会挑几个让候选人解释。常见传播行为有 REQUIRED、REQUIRES_NEW、NESTED、MANDATORY 等。REQUIRED 表示如果没有事务就新建有就加入REQUIRES_NEW 表示挂起当前事务新起一个独立事务NESTED 则表示嵌套事务内层回滚可以只回滚保存点不影响外层。这里很多人把 NESTED 和 REQUIRES_NEW 混淆其实 NESTED 依赖 JDBC 的 savepoint性能和数据源支持上有差异。事务不要滥用我在项目里见过最典型的问题是一个方法里调用外部接口外部接口超时 30 秒整个事务被拖住数据库连接一直被占用最后连接池耗尽。所以长事务一定要拆解外部调用尽量放到事务外层异步处理。6.3 数据库读写分离的思路数据库读写分离是一个偏架构的考点面试出现频率不低。SpringBoot 并没有内置读写分离需要自己实现动态数据源。经典思路是继承 AbstractRoutingDataSource重写 determineCurrentLookupKey 方法通过 ThreadLocal 记录当前应该走主库还是从库。实现要点有几个。第一数据源配置通常用 HikariCP 或 Druid 创建 master 和 slave 两个独立数据源再把它们放进一个目标数据源 Map交给路由数据源管理。第二通过 AOP 拦截 Service 或 Mapper 层方法按方法名或注解切换数据源策略。比如 select、get、query 开头的走从库其他走主库。第三事务和路由的兼容问题一旦方法上开启事务数据源必须在事务开启前确定否则可能全程走主库。我自己的经验是小项目不要轻易上读写分离主从延迟、数据一致性排查成本很高。面试能讲清楚思路和关键点就够落地时务必评估业务必要性。提示多数据源场景下Transactional 默认绑定主数据源的事务管理器。如果读写分离后出现“查到的是旧数据”的诡异现象先检查是不是事务将读写全部路由到了主库。7. 高频考点速查与日常小技巧7.1 面试高频题速查表这部分我把 SpringBoot 面试中出现频率最高的题目和参考回答角度整理成速查表适合打印出来贴在工位上。面试问题核心回答要点自动装配原理注解组合、AutoConfigurationImportSelector、imports 文件、条件注解Starter 和依赖的区别Starter 是依赖加自动配置的组合解决“引入即用”为什么 java -jar 能启动可执行 Jar 嵌套结构、JarLauncher、自定义类加载器默认 CGLIB 代理原因SpringBoot 2.0 后 proxyTargetClass 默认 true可拦截非接口方法yml 和 properties 区别结构表达、缩进、松散绑定支持配置优先级命令行 环境变量 profile 文件 主配置文件Value 与 ConfigurationProperties 对比单个值 vs 结构化绑定、SpEL 支持内嵌容器替换排除 Tomcat引入 Jetty/Undertow starterprofile 多环境spring.profiles.active、配置文件的叠加关系Transactional 失效场景同类调用、final/private 方法、异常被吞、自调用SpringBoot 3.x 新特性Jakarta EE 命名空间、AOT 编译、GraalVM 支持如何打 war 包部署打包方式改为 war、继承 SpringBootServletInitializer这张表只是索引每个知识点要能展开讲 2 到 3 分钟面试才算真正过关。千万不要背成“茴香豆的茴字有几种写法”要让面试官感到你真的在项目里这么用过。7.2 整合第三方组件的通用套路SpringBoot 的热搜词里大量涉及 ES、Redis、ActiveMQ、Kettle、视频转码、Mqtt 等组件的整合。很多人每整合一个组件就要查一遍教程其实底层套路是通用的。第一步确认官方是否提供 starter有就优先用。例如 spring-boot-starter-data-redis。没有就必须引入第三方 starter比如 mybatis-spring-boot-starter。第二步看自动配置生效后的默认 Bean 是什么通过自动装配原理判断自己还需要补充哪些配置。比如 Redis 默认提供 RedisTemplate但默认序列化器是 JdkSerializationRedisSerializer一般得自己定制 JSON 序列化器。第三步补全业务封装层比如封装 RedisUtil、MQ 消息监听器等。这套流程熟了之后整合新组件的心态会完全不一样从“到处搜教程”变成“看官方文档、找自动配置类、验证默认行为”。这其实就是 SpringBoot 框架给开发者的最大赋能把重复配置标准化让开发者专注于差异逻辑。举例来说整合 Elasticsearch 时先引入 spring-boot-starter-data-elasticsearch再用 Document 注解声明实体然后注入 ElasticsearchRestTemplate。真正费时间的是对查询 DSL 的封装比如 bool 查询、分词匹配规则这些和 SpringBoot 关系不大但面试时如果你能顺带提一句“ES 的索引设计要和业务查询场景匹配”就显得很有实战经验。7.3 一些不起眼但很实用的小技巧平时项目里有很多小技巧单拎出来不复杂但在面试实战中可以作为一种加分项展示。第一个是自定义启动 Banner用在线 Banner 生成器把文字转成 ASCII 艺术字放进 resources 目录下的 banner.txt启动时就会展示自己的标识。这虽然不能提升系统性能但能体现工程细节方面的注意。第二个是引入外部 Jar 包。Maven 项目里如果依赖了一个不在中央仓库的私有 Jar最简单的做法是安装到本地仓库mvn install:install-file -Dfilexxx.jar -DgroupIdcom.example -DartifactIdxxx -Dversion1.0 -Dpackagingjar然后按坐标正常引用。但要注意这种本地安装只对当前机器有效团队协作时最好搭建私有仓库否则其他人构建时会直接报缺失依赖。第三个是处理文档型接口的开关问题。新项目里如果引入了 springdoc 或 knife4j不想在某个环境暴露接口文档可以通过 springdoc.api-docs.enabledfalse 和 springdoc.swagger-ui.enabledfalse 关闭。别小看这个小配置生产环境暴露接口文档是我见过最常见的低级安全问题之一。写在后面的一些个人经验整理的这些知识点大都不是靠背诵记下来的而是在项目里一个个踩坑踩出来的。我自己带项目时有个习惯每次遇到“配置不生效”“代理不进切面”“事务没回滚”这类问题都会把排查过程整理成笔记标注根因和复原方式。时间久了这些笔记就成了面试时最自然的素材库因为面试官极爱问“你遇到过什么问题、怎么排查的”这时候能讲出具体失败案例和解决路径的候选人明显比只会背官方文档的人更有说服力。这套复习思路同样适用于那些没时间做系统复习的人与其花几个小时漫无目的地刷题不如把自动装配原理、Starter 设计、内嵌容器、配置体系、AOP 代理、数据访问这六大板块过一遍再结合自己项目里的实际报错仔细想一遍。面试前再做一轮快问快答基本就能稳住。最后再分享一个小技巧面试聊 SpringBoot 时不管问题多基础都尽量往“启动过程、运行时 Bean 生命周期、配置加载顺序”这三条主线上靠。SpringBoot 的一切特性都能在这三条主线上找到落脚点这也是 SpringBoot 框架设计得最出彩的地方。
返回列表