datai/datai-scenes/datai-scene-salesforce/docs/design/2026-02-03-002-07-WSDL转换-设计.md

28 KiB
Raw Permalink Blame History

设计文档 - WSDL 转换功能

元数据

  • 需求编号002-07
  • 创建时间2026-02-03
  • 创建人AI Assistant
  • 状态:进行中

设计概述

本设计文档描述 Salesforce Apex WSDL 转换功能的实现方案。WSDL 转换功能提供从 URL 或 XML 转换 WSDL 到 Apex 代码、查询转换历史、获取转换详情、下载生成的 Apex 类代码等功能。本功能基于 Salesforce Apex API 的 WSDL 转换相关接口,使用 apex.jar 中的 WsdlToApexInfo、WsdlToApexResult 等类,通过 ApexConnectionFactory 获取 SoapConnection 实例进行 WSDL 转换操作,并将转换结果存储到数据库中。

架构设计

系统架构

┌─────────────────────────────────────────────────────────────┐
│                    前端应用层 (Vue 3)                        │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                  Controller 层                              │
│         ApexWsdlController.java                              │
│  - 从 URL 转换 WSDL                                        │
│  - 从 XML 转换 WSDL                                        │
│  - 查询转换历史                                              │
│  - 获取转换详情                                              │
│  - 下载 Apex 类代码                                          │
│  - 批量下载 Apex 类代码                                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   Service 层                                │
│         IApexWsdlService.java                                │
│         ApexWsdlServiceImpl.java                             │
│  - 从 URL 转换 WSDL                                        │
│  - 从 XML 转换 WSDL                                        │
│  - 查询转换历史                                              │
│  - 获取转换详情                                              │
│  - 数据转换和格式化                                          │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   Mapper 层                                 │
│    DataiApexWsdlConversionMapper.java                      │
│    DataiApexWsdlClassMapper.java                          │
│  - 数据库访问                                                │
│  - SQL 映射                                                  │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   Entity 层                                 │
│    DataiApexWsdlConversion.java                           │
│    DataiApexWsdlClass.java                               │
│  - 数据模型                                                  │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   数据库层                                  │
│    datai_apex_wsdl_conversion 表                        │
│    datai_apex_wsdl_class 表                             │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│              Salesforce Apex API                            │
│         ApexConnectionFactory                               │
│         SoapConnection                                      │
│         WsdlToApexInfo, WsdlToApexResult                   │
└─────────────────────────────────────────────────────────────┘

模块架构

模块datai-salesforce-apex

包结构

com.datai.apex
├── controller
│   └── ApexWsdlController.java              # WSDL 转换控制器
├── service
│   ├── IApexWsdlService.java                # WSDL 转换服务接口
│   └── impl
│       └── ApexWsdlServiceImpl.java         # WSDL 转换服务实现
├── mapper
│   ├── DataiApexWsdlConversionMapper.java  # WSDL 转换记录 Mapper
│   └── DataiApexWsdlClassMapper.java      # WSDL 转换生成的 Apex 类 Mapper
├── domain
│   ├── DataiApexWsdlConversion.java       # WSDL 转换记录实体
│   └── DataiApexWsdlClass.java           # WSDL 转换生成的 Apex 类实体
└── model
    ├── dto
    │   ├── ConvertWsdlFromUrlDto.java    # 从 URL 转换 WSDL 请求 DTO
    │   ├── ConvertWsdlFromXmlDto.java    # 从 XML 转换 WSDL 请求 DTO
    │   └── QueryConversionsDto.java       # 查询转换历史请求 DTO
    └── vo
        ├── WsdlConversionVo.java          # WSDL 转换记录 VO
        ├── WsdlConversionDetailVo.java    # WSDL 转换详情 VO
        ├── WsdlClassVo.java             # WSDL 转换生成的 Apex 类 VO
        └── WsdlToApexResultVo.java     # WSDL 转换结果 VO

数据流

1. 从 URL 转换 WSDL 流程
   客户端发送 POST /api/apex/wsdl/convert-from-url
   ↓
   Controller 接收请求,参数校验
   ↓
   Controller 调用 Service.convertWsdlFromUrl(wsdlUrl, packageName, generateAsync)
   ↓
   Service 通过 ApexConnectionFactory 获取 SoapConnection
   ↓
   Service 构建 WsdlToApexInfo 对象
   ↓
   Service 调用 SoapConnection.wsdlToApex(info)
   ↓
   Service 解析转换结果Apex 类列表、警告、错误)
   ↓
   Service 保存转换结果到数据库
   ↓
   Service 返回结果给 Controller
   ↓
   Controller 封装统一响应格式返回给客户端

2. 从 XML 转换 WSDL 流程
   客户端发送 POST /api/apex/wsdl/convert-from-xml
   ↓
   Controller 接收请求,参数校验
   ↓
   Controller 调用 Service.convertWsdlFromXml(wsdlXml, packageName, generateAsync)
   ↓
   Service 通过 ApexConnectionFactory 获取 SoapConnection
   ↓
   Service 构建 WsdlToApexInfo 对象
   ↓
   Service 调用 SoapConnection.wsdlToApex(info)
   ↓
   Service 解析转换结果Apex 类列表、警告、错误)
   ↓
   Service 保存转换结果到数据库
   ↓
   Service 返回结果给 Controller
   ↓
   Controller 封装统一响应格式返回给客户端

3. 查询转换历史流程
   客户端发送 GET /api/apex/wsdl/conversions?status=success&page=1&pageSize=20
   ↓
   Controller 接收请求,参数校验
   ↓
   Controller 调用 Service.queryConversions(startTime, endTime, page, pageSize)
   ↓
   Service 通过 Mapper 查询数据库
   ↓
   Service 分页处理
   ↓
   Service 返回结果给 Controller
   ↓
   Controller 封装统一响应格式返回给客户端

4. 获取转换详情流程
   客户端发送 GET /api/apex/wsdl/conversion-detail?conversionId=1
   ↓
   Controller 接收请求,参数校验
   ↓
   Controller 调用 Service.getConversionDetail(conversionId)
   ↓
   Service 通过 Mapper 查询转换记录
   ↓
   Service 通过 Mapper 查询生成的 Apex 类列表
   ↓
   Service 组装转换详情 VO
   ↓
   Service 返回结果给 Controller
   ↓
   Controller 封装统一响应格式返回给客户端

5. 下载 Apex 类代码流程
   客户端发送 GET /api/apex/wsdl/download-class?classId=1
   ↓
   Controller 接收请求,参数校验
   ↓
   Controller 调用 Service.getClassCode(classId)
   ↓
   Service 通过 Mapper 查询 Apex 类代码
   ↓
   Service 返回类代码给 Controller
   ↓
   Controller 返回文件流给客户端

6. 批量下载 Apex 类代码流程
   客户端发送 GET /api/apex/wsdl/download-classes?conversionId=1
   ↓
   Controller 接收请求,参数校验
   ↓
   Controller 调用 Service.getClassCodesByConversionId(conversionId)
   ↓
   Service 通过 Mapper 查询所有 Apex 类代码
   ↓
   Service 打包成 ZIP 文件
   ↓
   Service 返回 ZIP 文件给 Controller
   ↓
   Controller 返回 ZIP 文件流给客户端

技术方案

技术选型

技术组件 版本要求 选择理由
Spring Boot 2.7.x 项目标准版本,提供完整的 Web 开发支持
Spring Security 2.7.x 提供认证和授权支持,与若依框架集成
MyBatis Plus 3.5.x 提供便捷的数据库操作,支持代码生成
MySQL 8.0+ 项目标准数据库,支持事务和索引
Swagger 3.0 提供接口文档,方便前端对接
Lombok 1.18.x 简化实体类代码
apex.jar Salesforce 提供的 Apex API JAR 包 提供 WSDL 转换相关 API
Apache Commons Compress 1.21+ 提供 ZIP 文件压缩功能

集成方案

1. 与 Salesforce Apex API 集成

连接获取

  • 使用 ApexConnectionFactory 获取 SoapConnection 实例
  • 固定使用 source org 类型
  • 连接管理由 SessionManager 负责

WSDL 转换 API 调用

  • SoapConnection.wsdlToApex(WsdlToApexInfo) - 转换 WSDL 到 Apex 代码

数据类型映射

  • WsdlToApexInfoConvertWsdlFromUrlDtoConvertWsdlFromXmlDto
  • WsdlToApexResultWsdlToApexResultVo
  • WsdlToApexResult.apexClassesDataiApexWsdlClass

2. 与若依框架集成

权限控制

  • 使用 @PreAuthorize 注解进行权限控制
  • 权限标识:apex:wsdl:convertapex:wsdl:query

统一响应

  • 使用 AjaxResult 封装统一响应格式
  • 成功响应:AjaxResult.success(data)
  • 失败响应:AjaxResult.error(message)

分页支持

  • 使用 PageUtils 进行分页处理
  • 使用 TableDataInfo 封装分页结果

异常处理

  • 使用 SalesforceAuthException 处理连接异常
  • 使用 SalesforceOperationException 处理操作异常
  • 使用 ServiceException 处理业务异常

3. 多线程安全设计

并发控制

  • 使用 ReentrantLock 保证多线程安全
  • 锁定范围WSDL 转换操作
  • 锁的粒度:方法级别

实现示例

private final ReentrantLock wsdlConversionLock = new ReentrantLock();

public WsdlToApexResult convertWsdlFromUrl(String wsdlUrl, String packageName, boolean generateAsync) {
    wsdlConversionLock.lock();
    try {
        SoapConnection connection = apexConnectionFactory.getConnection("source");
        WsdlToApexInfo info = new WsdlToApexInfo();
        info.setWsdlUrl(wsdlUrl);
        info.setPackageName(packageName);
        info.setGenerateAsync(generateAsync);
        
        WsdlToApexResult result = connection.wsdlToApex(info);
        
        saveConversionResult(info, result);
        
        return result;
    } finally {
        wsdlConversionLock.unlock();
    }
}

数据模型

数据库表设计

1. datai_apex_wsdl_conversionWSDL 转换记录表)

表名datai_apex_wsdl_conversion

字段说明

字段名 类型 长度 必填 默认值 说明
id BIGINT - AUTO_INCREMENT 主键 ID
wsdl_url VARCHAR 500 - WSDL URL
package_name VARCHAR 255 - 包名称
generate_async BOOLEAN - false 是否生成异步方法
status VARCHAR 50 - 转换状态success/failed
error_message TEXT - - 错误消息
user_id VARCHAR 50 - 用户 ID
dept_id BIGINT - - 部门 ID
create_by VARCHAR 64 - 创建人
create_time DATETIME - CURRENT_TIMESTAMP 创建时间
update_by VARCHAR 64 - 更新人
update_time DATETIME - CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 更新时间
del_flag TINYINT 1 0 删除标志0 正常 1 删除)

索引设计

  • 主键索引:PRIMARY KEY (id)
  • 普通索引:idx_user_id (user_id)
  • 普通索引:idx_dept_id (dept_id)
  • 普通索引:idx_create_time (create_time)
  • 普通索引:idx_status (status)

2. datai_apex_wsdl_classWSDL 转换生成的 Apex 类表)

表名datai_apex_wsdl_class

字段说明

字段名 类型 长度 必填 默认值 说明
id BIGINT - AUTO_INCREMENT 主键 ID
conversion_id BIGINT - - 转换 ID外键关联 datai_apex_wsdl_conversion.id
class_name VARCHAR 255 - 类名称
class_code LONGTEXT - - 类代码
create_time DATETIME - CURRENT_TIMESTAMP 创建时间

索引设计

  • 主键索引:PRIMARY KEY (id)
  • 普通索引:idx_conversion_id (conversion_id)
  • 普通索引:idx_class_name (class_name)
  • 外键约束:CONSTRAINT fk_wsdl_class_conversion FOREIGN KEY (conversion_id) REFERENCES datai_apex_wsdl_conversion (id) ON DELETE CASCADE

表关系

  • datai_apex_wsdl_conversiondatai_apex_wsdl_class 是一对多关系
  • 一个转换记录可以包含多个生成的 Apex 类
  • 删除转换记录时,级联删除对应的 Apex 类

实体类设计

1. DataiApexWsdlConversionWSDL 转换记录实体)

类路径com.datai.apex.domain.DataiApexWsdlConversion

类定义

package com.datai.apex.domain;

import com.baomidou.mybatisplus.annotation.*;
import com.datai.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
@TableName("datai_apex_wsdl_conversion")
public class DataiApexWsdlConversion extends BaseEntity {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("wsdl_url")
    private String wsdlUrl;

    @TableField("package_name")
    private String packageName;

    @TableField("generate_async")
    private Boolean generateAsync;

    @TableField("status")
    private String status;

    @TableField("error_message")
    private String errorMessage;

    @TableField("user_id")
    private String userId;

    @TableField("dept_id")
    private Long deptId;

    @TableLogic
    @TableField("del_flag")
    private Integer delFlag;
}

2. DataiApexWsdlClassWSDL 转换生成的 Apex 类实体)

类路径com.datai.apex.domain.DataiApexWsdlClass

类定义

package com.datai.apex.domain;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.util.Date;

@Data
@TableName("datai_apex_wsdl_class")
public class DataiApexWsdlClass {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("conversion_id")
    private Long conversionId;

    @TableField("class_name")
    private String className;

    @TableField("class_code")
    private String classCode;

    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private Date createTime;
}

接口设计

RESTful API 设计

1. 从 URL 转换 WSDL

接口POST /api/apex/wsdl/convert-from-url

功能:从 URL 转换 WSDL 到 Apex 代码

请求参数

{
  "wsdlUrl": "https://example.com/service?wsdl",
  "packageName": "com.example",
  "generateAsync": true
}

响应示例

{
  "code": 200,
  "message": "WSDL 转换成功",
  "data": {
    "apexClasses": [
      "public class ExampleService { ... }",
      "public class ExampleServiceRequest { ... }"
    ],
    "warnings": [
      "警告:某些类型可能无法完全转换"
    ],
    "errors": [],
    "conversionId": 1,
    "success": true
  }
}

权限@PreAuthorize("@ss.hasPermi('apex:wsdl:convert')")

2. 从 XML 转换 WSDL

接口POST /api/apex/wsdl/convert-from-xml

功能:从 XML 转换 WSDL 到 Apex 代码

请求参数

{
  "wsdlXml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>...",
  "packageName": "com.example",
  "generateAsync": false
}

响应示例

{
  "code": 200,
  "message": "WSDL 转换成功",
  "data": {
    "apexClasses": [
      "public class ExampleService { ... }",
      "public class ExampleServiceRequest { ... }"
    ],
    "warnings": [],
    "errors": [],
    "conversionId": 2,
    "success": true
  }
}

权限@PreAuthorize("@ss.hasPermi('apex:wsdl:convert')")

3. 查询 WSDL 转换历史

接口GET /api/apex/wsdl/conversions

功能:查询 WSDL 转换历史记录

请求参数

参数名 类型 必填 说明
startTime String 开始时间yyyy-MM-dd HH:mm:ss
endTime String 结束时间yyyy-MM-dd HH:mm:ss
status String 转换状态success/failed
page Integer 页码,默认为 1
pageSize Integer 每页大小,默认为 20

请求示例

GET /api/apex/wsdl/conversions?status=success&page=1&pageSize=20

响应示例

{
  "code": 200,
  "message": "查询成功",
  "data": {
    "total": 50,
    "page": 1,
    "pageSize": 20,
    "records": [
      {
        "id": 1,
        "wsdlUrl": "https://example.com/service?wsdl",
        "packageName": "com.example",
        "generateAsync": true,
        "status": "success",
        "errorMessage": null,
        "createTime": "2026-02-03 10:00:00",
        "userId": "005xx0000001Gw2EAS"
      }
    ]
  }
}

权限@PreAuthorize("@ss.hasPermi('apex:wsdl:query')")

4. 获取 WSDL 转换详情

接口GET /api/apex/wsdl/conversion-detail

功能:获取 WSDL 转换详情(包括生成的 Apex 类代码)

请求参数

参数名 类型 必填 说明
conversionId Long 转换 ID

请求示例

GET /api/apex/wsdl/conversion-detail?conversionId=1

响应示例

{
  "code": 200,
  "message": "查询成功",
  "data": {
    "id": 1,
    "wsdlUrl": "https://example.com/service?wsdl",
    "packageName": "com.example",
    "generateAsync": true,
    "status": "success",
    "errorMessage": null,
    "createTime": "2026-02-03 10:00:00",
    "userId": "005xx0000001Gw2EAS",
    "apexClasses": [
      {
        "id": 1,
        "conversionId": 1,
        "className": "ExampleService",
        "classCode": "public class ExampleService { ... }",
        "createTime": "2026-02-03 10:00:00"
      },
      {
        "id": 2,
        "conversionId": 1,
        "className": "ExampleServiceRequest",
        "classCode": "public class ExampleServiceRequest { ... }",
        "createTime": "2026-02-03 10:00:00"
      }
    ]
  }
}

权限@PreAuthorize("@ss.hasPermi('apex:wsdl:query')")

5. 下载生成的 Apex 类代码

接口GET /api/apex/wsdl/download-class

功能:下载生成的 Apex 类代码

请求参数

参数名 类型 必填 说明
classId Long 类 ID

请求示例

GET /api/apex/wsdl/download-class?classId=1

响应示例

Content-Type: text/plain
Content-Disposition: attachment; filename="ExampleService.cls"

public class ExampleService { ... }

权限@PreAuthorize("@ss.hasPermi('apex:wsdl:query')")

6. 批量下载 Apex 类代码

接口GET /api/apex/wsdl/download-classes

功能:批量下载转换生成的所有 Apex 类代码

请求参数

参数名 类型 必填 说明
conversionId Long 转换 ID

请求示例

GET /api/apex/wsdl/download-classes?conversionId=1

响应示例

Content-Type: application/zip
Content-Disposition: attachment; filename="apex-classes.zip"

[ZIP 文件包含所有生成的 Apex 类]

权限@PreAuthorize("@ss.hasPermi('apex:wsdl:query')")

实现要点

1. WSDL 转换核心实现

@Service
@Slf4j
public class ApexWsdlServiceImpl extends ServiceImpl<DataiApexWsdlConversionMapper, DataiApexWsdlConversion> implements IApexWsdlService {
    
    private static final String ORG_TYPE = "source";
    
    @Autowired
    private ApexConnectionFactory apexConnectionFactory;
    
    @Autowired
    private DataiApexWsdlConversionMapper apexWsdlConversionMapper;
    
    @Autowired
    private DataiApexWsdlClassMapper apexWsdlClassMapper;
    
    private final ReentrantLock wsdlConversionLock = new ReentrantLock();
    
    @Override
    public WsdlToApexResultVo convertWsdlFromUrl(String wsdlUrl, String packageName, boolean generateAsync) {
        wsdlConversionLock.lock();
        try {
            SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
            WsdlToApexInfo info = new WsdlToApexInfo();
            info.setWsdlUrl(wsdlUrl);
            info.setPackageName(packageName);
            info.setGenerateAsync(generateAsync);
            
            WsdlToApexResult result = connection.wsdlToApex(info);
            
            DataiApexWsdlConversion conversion = saveConversionResult(info, result);
            
            return convertToWsdlToApexResultVo(result, conversion.getId());
        } catch (ConnectionException e) {
            throw new SalesforceAuthException("APEX_WSDL_007", "连接失败", e);
        } catch (Exception e) {
            throw new SalesforceOperationException("APEX_WSDL_006", "WSDL 转换失败", e);
        } finally {
            wsdlConversionLock.unlock();
        }
    }
    
    @Override
    public WsdlToApexResultVo convertWsdlFromXml(String wsdlXml, String packageName, boolean generateAsync) {
        wsdlConversionLock.lock();
        try {
            SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
            WsdlToApexInfo info = new WsdlToApexInfo();
            info.setWsdlXml(wsdlXml);
            info.setPackageName(packageName);
            info.setGenerateAsync(generateAsync);
            
            WsdlToApexResult result = connection.wsdlToApex(info);
            
            DataiApexWsdlConversion conversion = saveConversionResult(info, result);
            
            return convertToWsdlToApexResultVo(result, conversion.getId());
        } catch (ConnectionException e) {
            throw new SalesforceAuthException("APEX_WSDL_007", "连接失败", e);
        } catch (Exception e) {
            throw new SalesforceOperationException("APEX_WSDL_006", "WSDL 转换失败", e);
        } finally {
            wsdlConversionLock.unlock();
        }
    }
    
    private DataiApexWsdlConversion saveConversionResult(WsdlToApexInfo info, WsdlToApexResult result) {
        DataiApexWsdlConversion conversion = new DataiApexWsdlConversion();
        conversion.setWsdlUrl(info.getWsdlUrl());
        conversion.setPackageName(info.getPackageName());
        conversion.setGenerateAsync(info.isGenerateAsync());
        conversion.setStatus(result.getErrors() == null || result.getErrors().isEmpty() ? "success" : "failed");
        conversion.setErrorMessage(result.getErrors() != null && !result.getErrors().isEmpty() ? 
            String.join(", ", result.getErrors()) : null);
        
        apexWsdlConversionMapper.insert(conversion);
        
        if (result.getApexClasses() != null && !result.getApexClasses().isEmpty()) {
            for (String apexClass : result.getApexClasses()) {
                DataiApexWsdlClass wsdlClass = new DataiApexWsdlClass();
                wsdlClass.setConversionId(conversion.getId());
                wsdlClass.setClassName(extractClassName(apexClass));
                wsdlClass.setClassCode(apexClass);
                apexWsdlClassMapper.insert(wsdlClass);
            }
        }
        
        return conversion;
    }
    
    private String extractClassName(String classCode) {
        Pattern pattern = Pattern.compile("public\\s+class\\s+(\\w+)");
        Matcher matcher = pattern.matcher(classCode);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return "UnknownClass";
    }
}

2. 固定使用 source org

所有方法中固定使用 "source" 作为 org 类型:

private static final String ORG_TYPE = "source";

SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);

3. 多线程安全设计

使用 ReentrantLock 保证 WSDL 转换操作的原子性:

private final ReentrantLock wsdlConversionLock = new ReentrantLock();

public WsdlToApexResultVo convertWsdlFromUrl(...) {
    wsdlConversionLock.lock();
    try {
        SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
        WsdlToApexResult result = connection.wsdlToApex(info);
        saveConversionResult(info, result);
        return convertToWsdlToApexResultVo(result, conversion.getId());
    } finally {
        wsdlConversionLock.unlock();
    }
}

4. 批量下载实现

@Override
public void downloadClassesByConversionId(Long conversionId, HttpServletResponse response) {
    List<DataiApexWsdlClass> classes = apexWsdlClassMapper.selectList(
        new LambdaQueryWrapper<DataiApexWsdlClass>()
            .eq(DataiApexWsdlClass::getConversionId, conversionId)
    );
    
    try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
        for (DataiApexWsdlClass wsdlClass : classes) {
            ZipEntry entry = new ZipEntry(wsdlClass.getClassName() + ".cls");
            zos.putNextEntry(entry);
            zos.write(wsdlClass.getClassCode().getBytes(StandardCharsets.UTF_8));
            zos.closeEntry();
        }
    } catch (IOException e) {
        throw new ServiceException("批量下载失败", e);
    }
    
    response.setContentType("application/zip");
    response.setHeader("Content-Disposition", "attachment; filename=\"apex-classes.zip\"");
}

相关文档