1. 为什么SAP和Java需要“握手”?聊聊Webservice这个“翻译官”
如果你在企业里待过,尤其是制造业、零售或者大型集团,大概率听过SAP这个名字。它就像公司的“大脑”,管着财务、供应链、生产这些核心命脉。而Java呢,更像是公司里那些灵活、敏捷的“手脚”,负责官网、移动App、数据分析平台这些对外对内的应用。问题来了,“大脑”和“手脚”怎么沟通?它们说的“语言”完全不同——SAP内部是ABAP,Java世界是Java,直接对话简直是鸡同鸭讲。
这时候,就需要一个靠谱的“翻译官”了。Webservice就是这个角色。我干了这么多年集成项目,可以很负责任地告诉你,在SAP和Java系统之间搞数据交换,Webservice至今仍然是最主流、最稳妥的方案之一,特别是对于那种不需要实时“秒级”同步,但要求稳定、可靠、跨防火墙的场景。你别看现在RESTful API很火,但在很多老牌企业的IT架构里,特别是SAP ECC或者早期S/4HANA环境,基于SOAP的Webservice依然是“老大哥”,很多核心业务流程的接口都靠它。
简单来说,Webservice就是一套标准协议。SAP这边把数据打包成一个格式标准的“XML包裹”,通过HTTP/HTTPS这个“快递网络”发出去。Java那边收到包裹,拆开一看,哦,是标准格式,我能看懂,然后处理完再打包一个标准格式的“回执包裹”发回来。整个过程,两边都不需要知道对方内部具体是怎么干活的,只要遵守共同的“打包规则”(WSDL)就行。这就完美解决了跨平台、跨语言的难题。
我刚开始接触这块的时候,也觉得那些WSDL、SOAP信封、命名空间挺头大的。但实际做几个项目下来,你会发现套路都是通的。这篇文章,我就把我这些年踩过的坑、总结出来的最佳实践,用最直白的话和你能直接抄的代码,给你讲明白。咱们不搞虚的,就聚焦两种最实际的场景:SAP主动调用Java服务 和 Java主动调用SAP服务。保证你看完就能上手试。
2. 环境准备:别在起跑线上摔跤
工欲善其事,必先利其器。和原始文章里提个大概不同,我这里得给你掰扯清楚版本,这太关键了。我见过太多人代码写得没问题,却因为环境版本对不上折腾一整天。
2.1 SAP端环境要点
首先看SAP。不管是SAP ECC 6.0还是S/4HANA,基本功能都有。但有个大坑需要注意:SAP NetWeaver的版本。我建议你直接用事务码 SOAMANAGER 来检查和管理Webservice,这是SAP提供的标准管理工具,比在后台配置里瞎找强多了。
重点来了:确认你的SAP系统已经激活了“Web服务基础架构”。怎么确认?执行事务码 SPROXY(企业服务仓库),如果能正常打开,说明基础功能是OK的。如果不行,可能需要联系 BASIS 同事检查一下相关的SC(软件组件)是否激活。这一步没做,后面的一切都是白搭。
另外,你需要有足够的权限。至少要有 S_ICF 权限来维护SICF服务(这是发布服务端点的地方),以及开发权限(如 S_DEVELOP)来创建函数模块和代理类。问你们团队的SAP顾问或者管理员要,不然你会卡在第一步。
2.2 Java端环境搭建
Java这边选择就多了。原始文章用的还是JDK 1.5和JBoss 5,那都是“上古时代”的配置了。我们现在当然要用更现代的。但为了和SAP端广泛使用的SOAP 1.1协议更好兼容,我推荐以下组合,这是经过大量项目验证的稳定搭配:
- JDK: 建议用 JDK 8 或 JDK 11 的LTS版本。这两个版本在企业界保有量极大,稳定性极高。别上来就用JDK 17+,有些老库的兼容性可能还有小问题。
- 应用服务器: Tomcat 9+ 或者 WildFly/JBoss EAP 7.x 就非常好。轻量级选Tomcat,需要完整的Java EE功能选WildFly。本文示例为了简单,会用Spring Boot内嵌Tomcat,这在实际项目中也非常普遍。
- 开发工具: IntelliJ IDEA 或 Eclipse 都可以。我个人强烈推荐IDEA,它对Maven和Spring Boot的支持更流畅。
- 核心依赖: 我们将使用 Apache CXF 或者 Spring Boot Web Services 来创建和调用Webservice。它们比古老的Axis2更强大、更易用,社区也活跃。本文主要用Spring Boot Web Services来演示,因为它和Spring Boot集成得几乎天衣无缝。
你不需要手动下载一堆jar包,用Maven或Gradle来管理依赖是最好的。下面是一个Maven pom.xml 的核心依赖片段,你可以直接复制:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.x</version> <!-- 使用一个稳定的2.7.x版本 -->
</parent>
<dependencies>
<!-- 用于创建WebService服务端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<!-- 用于根据WSDL生成Java代码 -->
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
</dependency>
<!-- 可选,用于测试调用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
把环境捋顺了,相当于给后面的操作铺了一条平路。接下来,咱们就进入实战,先看SAP如何调用我们写的Java服务。
3. 实战一:SAP调用Java提供的Webservice
这个场景很常见:SAP里发生了一个业务事件(比如创建了一个采购订单),需要立即通知到一个Java写的供应商管理系统或者工作流引擎。
3.1 第一步:用Spring Boot快速创建一个Webservice端点
首先,我们在Java这边创建一个服务。我们模拟一个简单的员工信息查询服务。在IDEA里新建一个Spring Boot项目,然后创建一个服务类。
这里有个关键点:定义好服务契约(Contract-First)。我们先定义好XML Schema(XSD),再根据它生成Java代码,这是最规范的做法。创建一个 xsd 文件夹,放一个 employee.xsd 文件:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://yourcompany.com/sap/employee"
xmlns:tns="http://yourcompany.com/sap/employee"
elementFormDefault="qualified">
<xs:element name="getEmployeeRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="employeeId" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="getEmployeeResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="employee" type="tns:employee"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="employee">
<xs:sequence>
<xs:element name="id" type="xs:string"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="department" type="xs:string"/>
<xs:element name="salary" type="xs:decimal"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
然后,通过Maven插件在编译时自动从XSD生成Java实体类。在 pom.xml 里加入:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>2.5.0</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<schemaDirectory>${project.basedir}/src/main/resources/xsd</schemaDirectory>
<outputDirectory>${project.basedir}/src/main/java</outputDirectory>
<clearOutputDir>false</clearOutputDir>
</configuration>
</plugin>
</plugins>
</build>
执行 mvn compile 后,就会生成对应的 Employee、GetEmployeeRequest 等类。接下来创建端点类:
package com.yourcompany.sapws.endpoint;
import com.yourcompany.sapws.xml.employee.GetEmployeeRequest;
import com.yourcompany.sapws.xml.employee.GetEmployeeResponse;
import com.yourcompany.sapws.xml.employee.Employee;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
@Endpoint // 标记这是一个WebService端点
public class EmployeeEndpoint {
private static final String NAMESPACE_URI = "http://yourcompany.com/sap/employee";
// 这个方法处理请求,根据employeeId返回员工信息
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "getEmployeeRequest")
@ResponsePayload
public GetEmployeeResponse getEmployee(@RequestPayload GetEmployeeRequest request) {
GetEmployeeResponse response = new GetEmployeeResponse();
Employee employee = new Employee();
// 这里应该是从数据库查询,我们模拟数据
if ("1001".equals(request.getEmployeeId())) {
employee.setId("1001");
employee.setName("张三");
employee.setDepartment("财务部");
employee.setSalary(new BigDecimal("15000.00"));
} else {
employee.setId(request.getEmployeeId());
employee.setName("未找到");
employee.setDepartment("N/A");
employee.setSalary(BigDecimal.ZERO);
}
response.setEmployee(employee);
return response;
}
}
最后,配置Spring Boot让服务发布出去。创建一个配置类:
package com.yourcompany.sapws.config;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;
@EnableWs // 启用Spring Web Services
@Configuration
public class WebServiceConfig {
@Bean
public ServletRegistrationBean<MessageDispatcherServlet> messageDispatcherServlet(ApplicationContext context) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(context);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean<>(servlet, "/ws/*");
}
@Bean(name = "employees") // WSDL的名称,访问路径会是 /ws/employees.wsdl
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema employeeSchema) {
DefaultWsdl11Definition definition = new DefaultWsdl11Definition();
definition.setPortTypeName("EmployeePort");
definition.setLocationUri("/ws");
definition.setTargetNamespace("http://yourcompany.com/sap/employee");
definition.setSchema(employeeSchema);
return definition;
}
@Bean
public XsdSchema employeeSchema() {
return new SimpleXsdSchema(new ClassPathResource("xsd/employee.xsd"));
}
}
启动Spring Boot应用,访问 http://localhost:8080/ws/employees.wsdl,你应该能看到自动生成的WSDL文件。把这个WSDL文件的URL记下来,比如 http://你的服务器IP:8080/ws/employees.wsdl,这是SAP那边要用的。
3.2 第二步:在SAP中创建服务代理并调用
现在切换到SAP GUI。假设我们要在SAP里通过员工号查询Java服务返回的详细信息。
- 创建代理类(Proxy): 执行事务码
SE80,在对象导航器里选择你的包(或本地对象),右键 -> 创建 -> 企业服务 -> 服务消费方。 - 导入WSDL: 在弹出的向导中,选择“外部WSDL/Schema”,在URL栏里粘贴上一步得到的Java服务的WSDL地址(例如
http://localhost:8080/ws/employees.wsdl)。点击继续,SAP会解析这个WSDL。 - 生成代理: 系统会提示你创建一个逻辑端口(Logical Port)和一个代理类。给代理类起个名字,比如
ZCO_EMPLOYEE_SERVICE。一路确认,激活生成的对象。这个过程中,SAP会自动根据WSDL生成ABAP的接口结构(就像Java里的实体类)和代理方法。 - 配置逻辑端口(Logical Port): 激活后,系统通常会提示你配置逻辑端口。或者你可以通过事务码
LPCONFIG找到刚才生成的逻辑端口进行配置。最关键的一步:在逻辑端口的配置里,找到“连接”或“地址”页签,将“端点URL”(Endpoint URL)修改为你的Java服务的实际SOAP地址。注意,这里不是WSDL地址,而是服务地址。对于Spring Boot WS,服务地址通常是http://localhost:8080/ws(不带.wsdl后缀)。这个地址通常可以在WSDL文件的<soap:address location>标签里找到。务必填对,否则调用会报“目标不可达”的错误。 - 编写ABAP调用代码: 代理类激活后,你就可以像调用普通ABAP函数一样调用它了。在SE38里创建一个报表程序试试:
REPORT ztest_call_java_ws.
DATA: lo_proxy TYPE REF TO zco_employee_service, " 代理对象
ls_request TYPE zget_employee_request, " 请求结构(自动生成)
ls_response TYPE zget_employee_response, " 响应结构(自动生成)
lx_fault TYPE REF TO cx_ai_system_fault. " 异常对象
TRY.
" 1. 创建代理实例
CREATE OBJECT lo_proxy.
" 2. 准备请求数据
ls_request-employee_id = '1001'. " 要查询的员工ID
" 3. 调用远程服务!
lo_proxy->get_employee(
EXPORTING
input = ls_request
IMPORTING
output = ls_response
).
" 4. 处理响应
IF ls_response IS NOT INITIAL AND ls_response-employee IS NOT INITIAL.
WRITE: / '员工ID:', ls_response-employee-id,
/ '姓名:', ls_response-employee-name,
/ '部门:', ls_response-employee-department,
/ '薪资:', ls_response-employee-salary.
ELSE.
WRITE: / '未找到该员工信息'.
ENDIF.
CATCH cx_ai_system_fault INTO lx_fault.
" 处理调用异常,比如网络不通、服务端错误等
WRITE: / '调用Webservice失败:', lx_fault->get_text( ).
ENDTRY.
运行这个程序,如果一切配置正确,你就能在SAP屏幕上看到从Java服务返回的员工“张三”的信息了。第一次成功调通的时候,那种成就感还是挺棒的。这里面的关键点就两个:WSDL要对,端点URL更要准。我见过太多人把WSDL地址当成服务地址填进去,然后对着报错发呆半天。
4. 实战二:Java调用SAP提供的Webservice
反过来,当Java应用需要从SAP主动获取数据(比如定时同步物料主数据)时,就需要Java去调用SAP发布的Webservice。
4.1 第一步:在SAP端创建并发布一个Webservice
首先,我们要在SAP里把一个功能暴露出来。最常用的方式就是将一个RFC函数模块发布为Webservice。
-
创建RFC函数模块: 事务码
SE37。我们创建一个简单的函数,根据物料号获取物料描述。函数名比如Z_GET_MATERIAL_DETAIL。记得函数属性要选“远程启用模块”(RFC)。IMPORTING参数:IV_MATNRTYPEMATNR(物料号)EXPORTING参数:EV_MAKTXTYPEMAKTX(物料描述)TABLES参数: (本例不用)EXCEPTIONS: 可以定义一些异常,比如MATERIAL_NOT_FOUND。 在源代码里写点简单的逻辑,比如从表MAKT里根据物料号和语言查描述。
-
发布为Webservice: 在SE37函数编辑界面,菜单栏:实用程序 -> 更多实用程序 -> 创建Web服务 -> 来自函数模块。跟着向导走:
- 选择“服务提供方”。
- 给服务定义起个名字,如
Z_WS_MATERIAL_QUERY。 - 一路默认,直到完成。系统会生成一个服务定义和绑定。
-
获取关键信息: 发布成功后,你需要两个至关重要的地址:
- WSDL地址: 执行事务码
SOAMANAGER,搜索你的服务名Z_WS_MATERIAL_QUERY,进入服务详情页,找到“WSDL文档”的链接。这个URL就是描述服务契约的。 - 服务端点地址: 同样在
SOAMANAGER或通过事务码SICF(维护HTTP服务)找到对应的服务节点,测试它,浏览器会打开一个测试页面,地址栏里的URL就是SOAP端点地址。它的格式通常像http://sap服务器:端口/sap/bc/srt/rfc/sap/z_ws_material_query/xxx/...。这个地址才是Java程序真正要连接的目标。
- WSDL地址: 执行事务码
注意: 生产环境中,务必使用HTTPS并配置好SAP系统的SSL证书。在SOAMANAGER里可以配置服务的安全策略,比如需要用户名密码认证。Java客户端调用时需要提供这些凭据。
4.2 第二步:在Java中生成客户端并调用
现在回到Java项目。我们不再用古老的Axis2 WSDL2Java,而是用更现代的 jaxws-maven-plugin 或 Spring Boot 的 wsimport 替代方案。
首先,在Maven pom.xml 里添加插件配置,让它在编译阶段自动从SAP的WSDL生成Java客户端代码:
<build>
<plugins>
<!-- 用于从SAP WSDL生成客户端代码 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxws-maven-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>generate-from-sap-wsdl</id>
<goals>
<goal>wsimport</goal>
</goals>
<configuration>
<!-- 这里替换成你从SAP SOAMANAGER获取的WSDL URL -->
<wsdlUrls>
<wsdlUrl>http://your-sap-server:8000/sap/bc/srt/wsdl/...&sap-client=100</wsdlUrl>
</wsdlUrls>
<!-- 生成的Java包名,避免冲突 -->
<packageName>com.yourcompany.sapclient.material</packageName>
<keep>true</keep>
<sourceDestDir>${project.build.sourceDirectory}</sourceDestDir>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
执行 mvn clean compile,插件会自动下载WSDL并生成一堆客户端类,如 ZWsMaterialQuery、ZGetMaterialDetail 等。
然后,我们写一个简单的Spring Bean或者Service类来调用它。这里演示一个更健壮的方式,使用 WebServiceTemplate:
package com.yourcompany.sapclient.service;
import com.yourcompany.sapclient.material.ZWsMaterialQuery;
import com.yourcompany.sapclient.material.ZGetMaterialDetail;
import com.yourcompany.sapclient.material.ZGetMaterialDetailResponse;
import org.springframework.stereotype.Service;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.soap.client.core.SoapActionCallback;
import javax.xml.bind.JAXBElement;
@Service
public class SapMaterialService {
private final WebServiceTemplate webServiceTemplate;
// 通常通过配置类注入一个配置好的WebServiceTemplate,这里为了演示简单构造
public SapMaterialService() {
this.webServiceTemplate = new WebServiceTemplate();
// 在实际项目中,这里应该配置一个通用的消息发送器(MessageSender),
// 并设置默认的URI和认证信息(如Basic Auth)。
}
public String getMaterialDescription(String materialNumber) {
try {
// 1. 创建请求对象(由wsimport生成)
ZGetMaterialDetail request = new ZGetMaterialDetail();
request.setIVMATNR(materialNumber);
// 2. 包装请求(根据生成的类结构可能需要)
// 注意:生成的类结构可能要求将请求对象包装在JAXBElement中,
// 具体取决于SAP WSDL的复杂程度。这里假设直接发送。
// 如果生成的是 ObjectFactory,可能需要这样:
// ObjectFactory factory = new ObjectFactory();
// JAXBElement<ZGetMaterialDetail> jaxbRequest = factory.createZGetMaterialDetail(request);
// 3. 设置服务端点地址(从SICF测试获取的那个)
String endpointUrl = "http://your-sap-server:8000/sap/bc/srt/rfc/sap/z_ws_material_query/...";
// 4. 发送SOAP请求并获取响应
ZGetMaterialDetailResponse response = (ZGetMaterialDetailResponse)
webServiceTemplate.marshalSendAndReceive(
endpointUrl,
request, // 或 jaxbRequest
new SoapActionCallback("urn:sap-com:document:sap:soap:functions:mc-style") // SOAP Action,通常在WSDL里能找到
);
// 5. 处理响应
if (response != null && response.getEVMAKTX() != null) {
return response.getEVMAKTX();
} else {
return "物料描述为空或未找到";
}
} catch (Exception e) {
// 处理网络异常、超时、SOAP Fault等
e.printStackTrace();
return "调用SAP服务失败: " + e.getMessage();
}
}
}
最后,你可以在一个Controller或者定时任务里调用这个 SapMaterialService。在实际项目中,你需要处理更多细节:
- 认证: SAP Webservice通常需要HTTP Basic认证或WS-Security。你需要为
WebServiceTemplate配置一个带有凭据的HttpComponentsMessageSender。 - 超时设置: 务必设置连接超时和读取超时,防止线程被挂死。
- 日志: 启用SOAP消息的日志拦截器,方便调试。Spring WS提供了
ClientInterceptor接口。 - 异常处理: 详细处理
SoapFaultClientException等异常,解析SAP返回的具体错误信息。
5. 避坑指南与性能优化
纸上谈兵终觉浅,绝知此事要踩坑。下面这些经验,都是我和团队用真金白银的时间换来的。
5.1 常见错误与排查
- 连接被拒绝/超时: 这是最常遇到的。首先用浏览器或Postman直接访问SAP的服务端点URL(不是WSDL),看是否能连通。检查网络、防火墙、SAP服务器端口(通常8000或443)。如果是Java调用SAP,还要检查SAP的
SICF事务码里,对应的服务节点是否被激活。 - WSDL导入失败: SAP生成的WSDL可能很大很复杂,包含很多内嵌的Schema。在SE80导入时,如果网络不稳定或WSDL中有不兼容的语法,可能会失败。可以尝试将WSDL文件先保存到本地,然后通过上传文件的方式导入。对于Java的
wsimport,如果遇到问题,可以尝试加上-XautoNameResolution参数,或者手动下载WSDL并修复一些简单的XML格式问题(注意备份原文件)。 - SOAP Action错误: 调用时报错“无法处理消息,因为操作不正确”。这需要检查SOAP请求头中的
SOAPAction字段是否与SAP服务期望的一致。在Spring WS的SoapActionCallback中设置正确的值,这个值通常在WSDL的<soap:operation>标签里。 - 数据类型映射错误: SAP的ABAP类型(如
DEC,QUAN)和Java类型(BigDecimal)的映射可能出问题。特别是数字、日期和时间。在生成客户端代码时,注意查看生成的JAXB注解,有时需要自定义适配器(XmlAdapter)来处理特殊的格式,比如SAP的日期YYYYMMDD转Java的LocalDate。
5.2 性能与稳定性建议
Webservice的性能瓶颈往往在XML的解析和网络传输上。
- 连接池: 频繁创建和销毁SOAP连接开销巨大。务必使用带连接池的HTTP客户端,如配置Spring
WebServiceTemplate时使用HttpComponentsMessageSender,并设置连接池参数(最大连接数、每路由最大连接数等)。 - 压缩: 如果传输的数据量较大(比如传输大批量物料列表),在SAP服务端和Java客户端同时启用GZIP压缩可以显著减少网络传输量。这需要在HTTP头中设置
Accept-Encoding和Content-Encoding。 - 异步调用: 对于不需要即时结果的调用(如日志记录、异步通知),强烈建议使用异步方式。SAP端可以配置异步的Webservice,Java端可以使用
AsyncWebServiceTemplate或直接使用@Async注解将调用任务提交到线程池,避免阻塞主线程。 - 超时与重试: 必须设置合理的连接超时(如5秒)和读取超时(如30秒)。对于因网络抖动导致的临时失败,可以实现一个简单的重试机制(如使用Spring Retry库),但要小心幂等性问题(同样的操作重复执行多次,结果应该一致)。
- 监控与日志: 记录每次调用的耗时、状态和请求/响应摘要(注意脱敏敏感数据)。这能帮你快速定位性能瓶颈和故障点。可以使用Spring的
ClientInterceptor来实现统一的日志拦截。
说到底,SAP和Java通过Webservice交互,技术本身并不神秘,就是一套标准的SOAP协议在支撑。真正的挑战在于对两边系统特性的理解、细致的环境配置和稳健的异常处理。从最简单的函数调用开始,一步步把路走通,再慢慢把上面的“补丁”一个个打好,你就能搭建起一条可靠的企业级系统集成通道。
441




被折叠的 条评论
为什么被折叠?



