news 2026/10/5 7:30:36

SpringBoot迁移宝兰德BES 9.5.5信创改造避坑实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot迁移宝兰德BES 9.5.5信创改造避坑实战指南

信创改造实战:SpringBoot项目迁移到宝兰德BES 9.5.5的完整避坑指南

1. 项目概述与迁移前思考

先说背景。之前接手了一个典型的SpringBoot 2.7.13微服务项目,原来部署方式是mvn package打成可执行jar包,扔到服务器上nohup java -jar就完事。但客户侧明确要求走信创路线,中间件必须国产化替换,目标平台锁定为宝兰德BES Application Server 9.5.5。这意味着原先的内嵌Tomcat方案不能用了,必须把SpringBoot应用以war包形式部署到BES上,由BES接管HTTP服务和类加载环境。

这里有个核心认知必须先纠正:SpringBoot项目迁移到BES,不是简单地把jar改成war扔进去就能跑。BES本质上是完整Java EE应用服务器,它有自己的一套类加载体系、JNDI命名空间、安全管理器和部署规范。内嵌Tomcat方式下很多“约定大于配置”的默认行为,在外部Servlet容器环境下会失效。比如SpringBoot自动装配里的ServletWebServerFactoryAutoConfiguration,在没有内嵌容器依赖时根本不会生效;再比如spring.main.web-application-type的推断逻辑,也会因为类路径变化而改变。

这篇博文适合谁看?一是正在做信创迁移、需要把存量SpringBoot服务部署到BES或其他国产中间件(如东方通TongWeb、金蝶天燕等)的开发人员;二是刚接触BES、想了解这个中间件部署机制的技术同学。我会把依赖改造、打包配置、控制台部署、以及我实际踩过的问题完整列出来,基本可以按图索骥。

先交代一下本次迁移涉及的核心组件清单,后面所有配置都是基于这个版本组合验证过的:

  • 操作系统:CentOS 7.6(x86_64)
  • JDK:1.8.0_202(BES 9.5.5内置OpenJDK或使用外部JDK均可,但推荐统一用外部JDK)
  • 应用框架:SpringBoot 2.7.13 + MyBatis-Plus 3.5.x
  • 目标中间件:宝兰德BES Application Server 9.5.5
  • 构建工具:Maven 3.6.3
  • 原部署方式:内嵌Tomcat 9.0.x jar包方式

2. BES 9.5.5环境准备与安装要点

2.1 获取安装包与基础环境

BES 9.5.5的安装包可以从宝兰德官方渠道获取,注意区分版本——企业版安装包通常包含管理控制台、节点代理和完整的Java EE容器;开发版可能阉割了部分集群能力,但单机部署完全够用。我这边拿到的是Linux 64位安装包,文件名类似bes-linux-x64-9.5.5.tar.gz。解压后的目录结构大概如下:

# 解压安装包 tar -zxvf bes-linux-x64-9.5.5.tar.gz -C /opt/bes/ cd /opt/bes/ ls -lh # 核心目录说明 # bin/ 启动脚本、管理命令 # domains/ 域目录(创建域之后生成) # lib/ 中间件核心依赖 # modules/ 模块化组件 # property/ 配置文件模板

注意:BES安装路径不要包含中文和特殊字符,建议统一放在/opt/或/usr/local/下,否则后续启动脚本会报路径解析异常。

2.2 创建域与启动管理控制台

BES的部署模型和WebLogic很像,核心概念就是“域(Domain)”。一个域包含管理服务器(AdminServer)和若干被管节点(Managed Server),所有应用部署操作都通过管理控制台完成。单机部署时,可以只创建一个域,管理服务器和业务服务器共用同一个实例,减少资源开销。

创建域的两种方式:

方式一:命令行方式

cd /opt/bes/bin ./asadmin create-domain --adminport 4848 --port 8080 mydomain

这里adminport是管理控制台端口,port是HTTP服务端口。asadmin是BES自带的域管理命令,类似GlassFish的asadmin,用起来很顺手。

方式二:交互式方式

直接执行./asadmin create-domain,不加参数,它会引导你输入域名、端口、管理员账号密码。我建议用交互式,方便设置管理员密码。

创建成功后,启动域:

./asadmin start-domain mydomain

启动日志会输出到domains/mydomain/logs/server.log,看到类似Server startup in [xxxx] milliseconds就说明启动成功了。此时访问http://服务器IP:4848可以进入管理控制台。

2.3 JDK版本选择与配坑

BES 9.5.5兼容JDK 8和JDK 11,但我的项目是SpringBoot 2.7.13(基础要求JDK 8),所以统一用JDK 8运行BES。这里有个比较隐蔽的坑:BES默认会使用自己内置的JRE(如果你没配置外部JDK),而这个内置JRE的版本不一定和应用的编译版本匹配,极容易导致UnsupportedClassVersionError。

建议强制指定外部JDK路径。修改domains/mydomain/config/domain.xml或启动脚本中的JAVA_HOME变量:

# 编辑启动脚本或设置环境变量 export JAVA_HOME=/usr/local/jdk1.8.0_202 export PATH=$JAVA_HOME/bin:$PATH # 重启域 ./asadmin restart-domain mydomain

另外,BES的JVM参数默认堆内存比较保守(通常只有256M),对微服务来说肯定不够。在控制台的“服务器配置 -> JVM设置”里,把-Xms1024m -Xmx2048m -XX:MaxMetaspaceSize=512m配置进去,确保应用运行期内存充足。

提示:BES对JDK的检测逻辑是从启动脚本所在环境变量中读取,不是读/etc/profile。所以每次启动前务必保证JAVA_HOME已经export到当前shell,否则它会fallback到内置JRE,这个问题非常隐蔽,排查起来也很耗时间。

3. SpringBoot项目依赖改造(核心章节)

3.1 打包方式从jar切换为war

迁移第一步是修改pom.xml的打包方式和依赖。原来的spring-boot-starter-web自带了内嵌Tomcat,在外部容器部署时会产生冲突,必须排除掉。

下面是我经过验证的完整依赖配置:

<packaging>war</packaging> <properties> <java.version>1.8</java.version> <spring-boot.version>2.7.13</spring-boot.version> </properties> <dependencies> <!-- Web依赖,排除内嵌Tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- BES API,用于自定义ServletInitializer时引用 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- 其他业务依赖:MyBatis-Plus、Redis、Nacos等按需保留 --> </dependencies>

关键点有两个:

  1. spring-boot-starter-tomcat必须显式排除。如果不排除而直接打包成war,BES部署时会检测到内嵌Tomcat相关的类,触发类加载冲突,表现通常是启动报NoClassDefFoundError: org/apache/catalina/...,或者应用启动到一半直接卡死。
  2. javax.servlet-api的scope必须是provided。因为这个依赖在BES运行时由容器提供,打不打包进去都行,但如果不加这个依赖,代码里引用ServletContext等API时编译会报错。为什么用javax而不是jakarta?因为我们用的SpringBoot 2.x底层是javax规范,BES 9.5.5同时支持javax和jakarta,但2.x用javax没问题。

3.2 SpringBootApplication启动类改造

SpringBoot jar方式部署的启动类通常长这样:

@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

这种写法在war方式部署时有一个致命问题:main方法只负责本地开发启动,在外部Servlet容器中启动时,容器根本不会执行main方法。容器启动的是ServletContainerInitializer体系,所以需要另外提供一个SpringBootServletInitializer的子类,重写configure方法:

@SpringBootApplication public class DemoApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // 参数必须传当前启动类 return builder.sources(DemoApplication.class); } }

注意:sources(DemoApplication.class)不能传错。有人习惯传this.getClass(),如果被代理类包装过,可能造成Spring扫描不到配置类,应用会启动但是无法加载任何Controller。

3.3 application.properties关键调整

接下来说说配置文件。jar方式下,很多外部化配置可以直接通过--spring.profiles.active=xxx方式和nohup命令传参;但war方式部署到BES后,应用是由BES的部署模块加载的,System.getProperty不一定能拿到JVM启动参数,所以有几种做法:

第一种(推荐):在application.properties中直接固定Profile和上下文路径。

# 指定当前环境为生产 spring.profiles.active=prod # 上下文路径,即访问根路径 server.servlet.context-path=/demo # 外部容器模式下,端口配置不生效,由容器负责监听 server.port=8080

这里强调一下:server.port在war包部署到外部容器后是无效的!因为端口由BES实例决定,不是应用自己决定。配置了也不报错,只是SpringBoot会忽略它。

第二种(进阶):如果多个应用部署到同一个BES,不同应用想要不同Profile,可以用spring.application.json环境变量或者JNDI自定义配置。这块后续在“多应用隔离”部分再展开,单应用部署先不折腾。

3.4 数据源与JNDI绑定(另一个大坑)

在jar方式下,数据源通常用spring.datasource.url、spring.datasource.username直接写在配置里,或者在Nacos配置中心统一管理。但企业级应用部署到BES后,通常走JNDI数据源方式——这是Java EE容器的标准玩法。原因很简单:运维可以通过控制台动态调整数据库连接池,不用重新打包重新部署;数据库密码可以存放于BES的配置领域,避免明文写在应用配置里。

BES控制台配置JNDI数据源:

  1. 进入控制台 -> 资源 -> JDBC -> 连接池 -> 新建。
  2. 数据源类型选择MySQL(我用的是MySQL 8.x,驱动类名com.mysql.cj.jdbc.Driver)。
  3. 配置JDBC URL、用户名、密码。
  4. 设置JNDI名称,比如jdbc/mydb。
  5. 点击“测试连接”,确认连通性。

应用侧改造可以二选一:

方案A:Spring Boot标准方式

# 使用JNDI数据源 spring.datasource.jndi-name=jdbc/mydb

这是最简洁的写法,SpringBoot的DataSourceAutoConfiguration会自动通过JndiDataSourceLookup去容器中查找数据源。

方案B:自定义DataSource配置类

@Configuration public class DataSourceConfig { @Bean @Primary public DataSource dataSource() throws NamingException { JndiDataSourceLookup lookup = new JndiDataSourceLookup(); return lookup.getDataSource("jdbc/mydb"); } }

如果你用了MyBatis-Plus,记得确保DataSource优先被注入到SqlSessionFactory中。方案A就走SpringBoot默认的自动装配路径,基本不用改代码;方案B适合那些需要自定义连接池参数(如最大连接数、超时时间)的场景。

我在实操中发现一个细节:BES的JNDI查找名称,必须和控制台上定义的名称完全一致,开头是java:comp/env/jdbc/mydb还是直接jdbc/mydb,取决于BES版本和配置方式。9.5.5版本在控制台创建连接池时,填入的JNDI名称直接写jdbc/mydb,应用侧写jdbc/mydb就能找到;但如果通过domain.xml手写资源,可能需要加java:comp/env/前缀。最稳妥的办法先用控制台创建,不要直接改XML。

4. 完整构建与BES部署实操

4.1 Maven构建war包

依赖都改好后,执行打包:

mvn clean package -Dmaven.test.skip=true -Pprod

构建成功后,war包位于target/目录,名称形如demo-1.0.0.war。注意SpringBoot的SpringBootServletInitializer机制下,war包在本地./target目录中会包含WEB-INF/lib-provided目录(存放provided依赖的jar),这些jar在部署到BES时不会使用,但保留在包内没问题,BES会自动忽略。

4.2 控制台部署war包

部署过程很简单:

  1. 登录BES管理控制台(http://IP:4848)。
  2. 左侧菜单选择“应用” -> “Web应用” -> “部署”。
  3. 上传war包,应用名称填demo(建议和war包名一致,这样上下文路径好控制)。
  4. 上下文根路径填/demo,对应访问地址是http://IP:8080/demo。
  5. 部署完成后点击“启动”。

如果一切正常,BES会解压war包,创建应用实例,通过SpringBootServletInitializer完成Spring容器的初始化。查看日志确认效果:

tail -f /opt/bes/domains/mydomain/logs/server.log

看到以下日志说明启动成功:

Root WebApplicationContext: initialization completed in 8122 ms BES Web Application Startup Time: 8000 ms

然后你就可以访问http://IP:8080/demo/验证接口了。

4.3 单机多实例部署模式

如果服务器资源充足,想在一台机器上跑多个BES实例做应用隔离或本地灰度,可以创建多个域,或者在同一域下创建多个独立实例。命令方式:

./asadmin create-node --nodeport 28181 mynode ./asadmin create-instance --node mynode --port 8081 instance1 ./asadmin create-instance --node mynode --port 8082 instance2

创建后分别启动:

./asadmin start-instance instance1 ./asadmin start-instance instance2

实际项目中,多实例的典型场景是“nacos注册中心同一个服务注册两个实例,实现本地双节点负载”。BES自身也有集群和负载均衡能力,但如果你的服务中间接入了Nacos/Feign,BES的集群LB反而可以不用,让Nacos层面的负载来分摊流量。

4.4 访问日志与并发参数调优

BES部署后,默认并发和线程池参数比较保守。如果你的SpringBoot应用压测发现吞吐上不去,先看BES的HTTP线程池配置:

控制台 -> 服务器配置 -> HTTP服务 -> 线程池,把最大线程数从默认值调高(比如调到400),然后配置连接超时和请求超时。这些参数对应domain.xml里类似这样:

<thread-pool max-thread-pool-size="400" min-thread-pool-size="20" idle-thread-timeout-seconds="1200"/>

访问日志默认是关闭的,排障时需要开启。在JVM选项里加系统属性:

-Dcom.bes.webserver.log.access.enabled=true -Dcom.bes.webserver.log.access.dir=/opt/bes/logs

开启后,每次HTTP请求会记录IP、时间、URL、状态码和耗时。排查某接口慢或客户端异常断开时,这份日志非常管用。

5. 实战中的常见问题与排查实录

5.1 类加载冲突:Jackson版本被容器覆盖

问题现象:应用启动报错,提示jackson-databind的某个类方法找不到,或者反序列化时直接抛出InvalidDefinitionException。定位后发现BES自身lib目录带了一份较旧的Jackson版本,由于BES的类加载策略是“父类优先”,应用lib中的新版本Jackson被旧版本覆盖。

解决方案:

  • 修改应用的WEB-INF/classes下的META-INF/MANIFEST.MF,通过Class-Path显式指定优先加载应用jar。这个在BES里对应“应用部署配置 -> 类加载策略”,可以设置为“子类优先”(即child-first)。
  • 或者在domain.xml中给特定应用添加如下配置:
<application name="demo" class-loader-policy="child-first"/>

如果不想动配置,还有一个土办法:把目标jar(如jackson-databind新版本)复制到/opt/bes/domains/mydomain/lib/目录,覆盖BES自带的旧版本。但这种方法会影响域内所有应用,改动面较大,如果不是确认全局兼容,不建议直接覆盖。

5.2 启动出现“No active profile set”或配置未加载

问题现象:应用日志打出No active profile set, falling back to 1 default profile,数据库连接失败,判断是官方配置没生效。

原因分析:war部署在BES时,SpringBoot的application.properties路径不受--spring.config.location控制,同时nacos地址等参数需要通过环境变量或JNDI传入。排查时先确认spring.profiles.active是否正确注入,再看nacos.config.server-addr是否可通过本地环境变量读到。

推荐做法:

# application.properties保持默认配置 spring.profiles.active=${SPRING_PROFILES_ACTIVE:prod} spring.cloud.nacos.config.server-addr=${NACOS_ADDR:127.0.0.1:8848}

这样BES的JVM系统属性或操作系统环境变量里配置SPRING_PROFILES_ACTIVE和NACOS_ADDR就能自动覆盖。如果环境变量也拿不到,就把NACOS_ADDR硬编码后重新打包,等后续有条件再接配置中心。

5.3 上传文件临时目录不存在

问题现象:通过接口上传文件(比如Excel导入),第一次报错Failed to create temp directory或者java.io.IOException: No space left on device,但磁盘空间明明够用。

原因分析:SpringBoot默认上传下载的临时目录取自javax.servlet.context.tempdir,BES为每个应用分配的临时目录在domains/mydomain/applications/demo/tmp,如果该目录不存在或权限不对,就会报这个错。

解决办法:

mkdir -p /opt/bes/domains/mydomain/applications/demo/tmp chown -R bes:bes /opt/bes/domains/mydomain/applications/

然后在应用配置里强制指定临时目录:

spring.servlet.multipart.temp-dir=/opt/bes/domains/mydomain/applications/demo/tmp

5.4 日志文件找不到了

jar方式下,logback-spring.xml里的日志路径通常写成相对路径./logs,在war方式下相对路径的“当前目录”是BES的bin目录或特定工作目录,结果日志文件被创建到奇怪的地方,甚至因为权限不足,直接抛异常无法输出日志。

经验做法:把logback-spring.xml中的日志路径改成绝对路径,或者通过LOG_HOME系统属性指定:

<property name="LOG_HOME" value="${LOG_HOME:-/opt/bes/domains/mydomain/logs/demo}"/> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/app.log</file> <!-- 其余配置省略 --> </appender>

然后启动BES前设置系统属性或环境变量:

export LOG_HOME=/opt/bes/domains/mydomain/logs/demo

5.5 集群部署时Session共享问题

如果有多个BES实例组成集群,默认情况下Session不会跨节点共享。SpringBoot应用如果用到HttpSession(登录验证码、权限暂存等),需要把Session共享方案迁移到容器级别。

BES自带集群Session复制功能,但配置比较繁琐;我们项目里选择在应用层解决:接入Redis统一存储Session。依赖如下:

<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>

配置:

spring.session.store-type=redis spring.redis.host=127.0.0.1 spring.redis.port=6379

这样不需要动BES集群配置,Session问题就直接绕开了。

6. 依赖配置与避坑清单速查

下面把本次改造的核心内容浓缩为一张速查表,方便你迁移时对照检查:

检查项jar包内嵌Tomcat方式war包部署BES方式备注
打包方式<packaging>jar</packaging><packaging>war</packaging>必须改
web-starterspring-boot-starter-webspring-boot-starter-web(排除tomcat)排除内嵌Tomcat
servlet-api依赖无需添加javax.servlet-api,scope=provided不添加则编译报错
启动类普通main方法继承SpringBootServletInitializer必须重写configure
server.port生效不生效,端口由BES管理配置了也不会启动监听
server.servlet.context-path生效建议在BES控制台配置上下文根配置可能被覆盖
数据源spring.datasource.url直接配置JNDI数据源spring.datasource.jndi-name也可以从Nacos读取
日志路径相对路径./logs必须使用绝对路径否则日志散落各处
JVM参数java -jar -XmxBES JVM设置中配置与控制台保持一致
上传临时目录Servlet容器自动管理需显式指定否则可能创建失败
Profile切换命令行--spring.profiles.active环境变量或JVM系统属性需保证可注入

这份表其实不限于BES,也适用于其他国产Java EE中间件,比如东方通TongWeb、金蝶天燕Apusic、中创InforSuite等,它们的部署方式和常用坑都高度相似,换成对应控制台操作就行。

7. 我踩过的最深的坑

最后分享一个让我排查了两天的典型问题,给你打个预防针。

我们是先从jar方式在本地mvn spring-boot:run调试,功能全通了,再改成war方式部署到BES。部署后应用界面能开,但所有接口返回的JSON中中文字段都变成了???或乱码。一开始怀疑是数据库连接字符集问题,检查characterEncoding=utf8也加了,没用;又怀疑是HttpMessageConverter配置问题,排查了一大圈。

最后用curl直接观察BES的响应头,发现Content-Type里带的是charset=ISO-8859-1,而不是预期的UTF-8。原来是BES的HTTP响应编码默认走ISO-8859-1,SpringBoot在外部容器下拿不到请求默认编码,所以返回体的编码被覆盖了。

解决办法:在BES控制台的“服务器配置 -> HTTP监听器 -> 编码”里把默认编码改成UTF-8,或者在应用的Filter里强制设置response.setCharacterEncoding("UTF-8")。后来为了避免每个接口都手动处理,我加了一个OncePerRequestFilter统一处理:

@Component public class CharacterEncodingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); filterChain.doFilter(request, response); } }

部署后再测,中文全部正常。

这个坑的教训是:中间件迁移过程中,“看似正常”比“直接报错”更可怕。报错时日志会给出线索,而编码这种隐性问题,很容易被误判为业务代码问题。建议你在做信创改造验证时,专门抽出测试用例覆盖中文字符、特殊符号、上传下载文件流、Session传递这几个高敏感场景,每一个都要盯紧。

迁移完成后,记得把BES日志接入你的ELK或日志平台,后续线上排查完全依赖它。祝你的信创改造之路一切顺利,少踩一些我踩过的坑。


提示:以上配置基于宝兰德BES 9.5.5版本,如果你使用的是其他版本,部分控制台路径和参数名可能会有差异,以官方文档为准。如果迁移过程中遇到其他问题,欢迎一起交流学习。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 7:28:31

VSCode C/C++调试配置化解:三文件协作与断点命中指南

VSCode调试C/C&#xff0c;说简单也简单&#xff0c;说麻烦是真麻烦。我见过太多人装了C/C插件就直接按F5&#xff0c;界面弹出一堆launch.json配置错误&#xff0c;或者编译通了却永远打不上断点&#xff0c;最后怀疑人生地回到Visual Studio的怀抱。其实C/C调试在VSCode里的核…

作者头像 李华
网站建设 2026/10/5 7:28:24

RestTemplate深度实战:从基础调用到文件上传与性能调优

restTemplate这个类&#xff0c;基本是Spring项目里绕不开的HTTP客户端工具。我接手过不少老项目&#xff0c;Controller里调第三方接口的代码十有八九是new RestTemplate然后getForObject&#xff0c;看起来一切正常&#xff0c;真跑到线上就开始出幺蛾子。这篇东西我想一次性…

作者头像 李华
网站建设 2026/10/5 7:27:47

FPGA控制DDR读写(AXI4总线接口)实战:从架构到调试全流程

从第一块FPGA板卡到现在&#xff0c;我接过不少和FPGA控制DDR读写相关的项目&#xff0c;最常见的是图像采集卡的帧缓存、高速ADC采样数据的暂存、还有软件无线电里的波形回放。DDR本身是一个相对成熟的内存颗粒&#xff0c;但真正要在FPGA工程里把读写跑稳、跑快&#xff0c;总…

作者头像 李华
网站建设 2026/10/5 7:27:03

OpenSSH升级实战指南:覆盖macOS、CentOS、Alibaba Cloud Linux、OpenEuler与Windows

1. 为什么OpenSSH升级让人又爱又怕做运维这些年&#xff0c;打交道最多的服务之一就是OpenSSH。说它重要吧&#xff0c;它重要到几乎所有服务器远程登录都靠它&#xff1b;说它烦吧&#xff0c;每次升级都像是在走钢丝——改错了配置文件、编译漏了依赖、重启sshd的时候断开会话…

作者头像 李华
网站建设 2026/10/5 7:26:55

LMS Virtual.Lab声学仿真结果自动导出:VBScript与Python二次开发实战

上个月处理一批整车声学包仿真&#xff0c;二十多个工况&#xff0c;每个工况要导出场点声压级曲线、1/3倍频程谱和几组云图&#xff0c;还要按客户模板整理成Excel。我本来打算手动导&#xff0c;导到第三个工况就放弃了——点鼠标点到手腕僵&#xff0c;文件名还容易漏。那会…

作者头像 李华