柚子蜜 - Java
type: channel
link: https://youthme.cn/category/Java/
items: Java 实现 Silk 音频编解码技术方案
items: 泛微OA对接飞书多维表格HTTP请求工具类
items: 2025年泛微OA专用接口数据附件入库更新工具类-多主键配置（原创）
items: 2025年泛微OA专用接口数据入库更新工具类-多主键配置（原创）
items: 2025年泛微OA专用接口数据入库工具类（原创）
items: 在已经定义MysqlDataSourceConfig并使用druid的情况下实现多数据源配置
items: JAVA快速开发微信机器人-新星框架（NewBot-Nova）
items: SpringBoot生成项目文件目录树
items: mvn install -Dmaven.test.skip=true
items: SpringBoot注入service为null（空指针异常）解决方案

Java 实现 Silk 音频编解码技术方案
type: item
date: 2025-11-15T20:13:00+08:00
description: 摘要本文介绍了一种基于 Java 的 Silk V3 音频编解码实现方案。该方案通过封装腾讯官方 Silk V3 编解码工具，实现了纯 Java 环境下的 WAV、PCM 和 Silk 格式音频的相互转换。方案采用完全对称的API设计，支持跨平台部署，并提供了统一的命名规范，可应用于即时通讯、语音消息处理等场景。一、背景介绍1.1 Silk 编解码器简介Silk 是由 Skype 开发、后被腾讯广泛应用的语音编解码器，具有以下特点：高压缩比：相比 PCM 格式可实现 20-30 倍的压缩；语音优化：专门针对人声频率范围进行优化；低延迟：适合实时语音通信场景；自适应码率：根据网络状况动态调整比特率。1.2 技术挑战Silk V3 官方实现为 C/C++ 代码，在 Java 环境中直接使用面临以下挑战：JNI 集成复杂：需要针对不同平台编译原生库；跨平台部署困难：Windows、Linux、macOS 需要分别处理；依赖管理复杂：第三方 Java 封装库存在版本兼容性问题；API 不统一：现有方案缺乏清晰的命名规范。二、技术方案2.1 整体架构本方案采用外部工具调用方式，通过 Java 进程管理机制调用 Silk V3 编解码工具，避免了 JNI 集成的复杂性。架构分为三层：工具层：Silk V3 编解码可执行文件；编解码层：SilkCodec 类封装编解码逻辑；转换层：AudioConverter 类提供高级音频转换接口。2.2 核心技术方案使用的主要技术包括：Java NIO：文件读写和临时文件管理；ProcessBuilder：外部进程调用和输出流管理；Java Sound API：WAV 音频文件解析和生成；自定义WAV解析器：处理大文件和特殊格式；ClassLoader：从 JAR 包中提取工具文件；平台检测：自动识别操作系统并选择对应工具。2.3 API 设计原则本方案采用以下API设计原则：完全对称：每种转换都支持 File→File、File→Bytes、Bytes→File、Bytes→Bytes 四种组合；统一命名：采用 {源格式}{源类型}To{目标格式}{目标类型} 格式；无重载：每个方法名唯一，避免调用歧义；固定采样率：统一使用 24kHz（微信标准）；明确后缀：时长计算方法使用 ByFile 和 ByBytes 后缀。三、实现细节3.1 工具管理3.1.1 工具提取机制系统启动时，从 JAR 包的 classpath 中提取 Silk 编解码工具到临时目录：private static File extractToolFromClasspath(String resourcePath, String executable) { InputStream is = SilkCodec.class.getClassLoader().getResourceAsStream(resourcePath); if (is == null) { return null; } if (toolsExtractDir == null) { synchronized (SilkCodec.class) { if (toolsExtractDir == null) { toolsExtractDir = Files.createTempDirectory("silk_tools_").toFile(); toolsExtractDir.deleteOnExit(); } } } File toolFile = new File(toolsExtractDir, executable); if (!toolFile.exists()) { Files.copy(is, toolFile.toPath(), StandardCopyOption.REPLACE_EXISTING); toolFile.setExecutable(true); toolFile.deleteOnExit(); } is.close(); return toolFile; }3.1.2 平台自适应根据操作系统类型选择对应的工具文件：private static String getToolPath(String tool) throws IOException { String os = System.getProperty("os.name").toLowerCase(); String toolsDir; String executable; if (os.contains("win")) { toolsDir = "windows"; executable = tool.equals("encoder") ? "silk_v3_encoder.e…

泛微OA对接飞书多维表格HTTP请求工具类
type: item
date: 2025-10-21T17:30:00+08:00
description: 目前有个需求是把泛微OA的需求台账同步到飞书多维表格里面，虽然飞书官方封装了对应的SDK，但是泛微OA只能通过本地jar包的形式导入，多有不便，并且SDK的文档也不全过于分散（可能是我没找到具体的点）。只能自己造一个轮子了，把常用的接口都封装了一下：[collapse status="true" title="FeiShuHttpService.java"]package weaver.interfaces.push.feishu; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import com.alibaba.fastjson.JSONObject; import java.util.HashMap; import java.util.Map; /** * @Author zF. * @ClassName FeiShuHttpService.java * @ProjectName ecology * @Description 飞书多维表格HTTP请求工具类 - 基于HTTP API实现多维表格的增删改查操作 */ public class FeiShuHttpService { // 飞书API基础URL private static final String FEISHU_BASE_URL = "https://open.feishu.cn/open-apis"; // 缓存访问令牌 private static volatile String cachedAccessToken; private static volatile long tokenExpireTime = 0; /** * 获取访问令牌 * * @return 访问令牌 */ public static String getAccessToken() { // 检查缓存是否有效 if (StrUtil.isNotBlank(cachedAccessToken) && System.currentTimeMillis() < tokenExpireTime) { return cachedAccessToken; } try { Map<String, String> config = FeiShuConfig.getAppConfig(); String appId = config.get("app_id"); String appSecret = config.get("app_secret"); if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) { throw new RuntimeException("飞书应用配置不完整"); } String url = FEISHU_BASE_URL + "/auth/v3/tenant_access_token/internal"; Map<String, Object> requestBody = new HashMap<>(); requestBody.put("app_id", appId); requestBody.put("app_secret", appSecret); HttpResponse response = HttpRequest.post(url) .header("Content-Type", "application/json; charset=utf-8") .body(JSONObject.toJSONString(requestBody)) .timeout(30000) .execute(); if (response.getStatus() == 200) { JSONObject result = JSONObject.parseObject(response.body()); if (result.getIntValue("code") == 0) { cachedAccessToken = result.getString("tenant_access_token"); // 设置过期时间（提前5分钟过期） int expire = result.getIntValue("expire"); tokenExpireTime = System.currentTimeMillis() + (expire - 300) * 1000L; System.out.println("飞书访问令牌获取成功"); return cachedAcc…

2025年泛微OA专用接口数据附件入库更新工具类-多主键配置（原创）
type: item
date: 2025-06-26T17:30:00+08:00
description: 在之前的两个工具类的基础上又封装了一个通用文件上传并入库工具类，完美解决了附件上传的问题。注意此工具类操作有点危险，原因是泛微OA的一些底层机制影响的。具体看代码吧：package com.api.zp.interfaces.util; import com.alibaba.fastjson.JSONObject; import com.api.doc.detail.service.DocAccService; import com.api.doc.detail.service.DocSaveService; import weaver.conn.RecordSet; import weaver.general.BaseBean; import weaver.hrm.User; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.*; /** * 支持： * 1. 附件上传到文档中心，返回docId * 2. 业务表数据插入/更新（唯一字段判断） * 3. 字段映射+自定义字段优先合并 * 4. 自动删除旧附件记录和文件 * * @Author zF. * @ClassName FileUploadProcessorUtil.java * @ProjectName 通用文件上传并入库工具类 */ public class FileUploadProcessorUtil { private static final BaseBean baseBean = new BaseBean(); /** * 之前的那些人写文件上传并入库都要重新了解一遍泛微OA的底层逻辑，还需要单独更新对应的两张文件表中的数据。 * 需要编写大量代码来实现这些操作，且极容易出错。在写附件上传的时候，由于没有文档，我阅读了本项目中大量前人留下的坑。 * 例如：docCategoryId这个字段原有的位置，因为泛微命名其为var1，所以不明其意。最后连线泛微的官方工作人员才知道那个位置是什么东西。 * 我在仔细了解了当前项目的交互逻辑之后，遂封装了这个工具类。从此以后，继任的开发者再也不需要关心泛微OA的底层逻辑。 * 只需要按照我要求的传参，即可直接实现附件上传功能。实在是太优雅啦，当然，如有兴趣你也可以直接阅读本工具类代码，注释非常详细。 */ /** * 通用文件上传+入库（支持自动上传文件并入库） * * @param body json数据体 * @param updateFields 字段映射 * @param uniqueFields 唯一字段 * @param tableName 业务表 * @param attachmentFieldName 附件字段名 * @param fileList 附件列表（每个map含fileUrl、fileName） * @param docCategoryId 文档目录ID * @param userId 用户ID * @param customFields 自定义字段 */ public static Map<String, Object> processDbDataAndUpsert(JSONObject body, Map<String, String> updateFields, List<String> uniqueFields, String tableName, String attachmentFieldName, List<Map<String, String>> fileList, int docCategoryId, int userId, Map<String, Object> customFields) { // 环境判断：只允许在生产环境（Linux）运行 String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("windows")) { // 原因是本地运行该工具类后，由于共用数据库的缘故。返回的imageFileId和docId并不是你上传的，而是线上生产环境其他业务真实上传的附件信息。 // 这是泛微OA的特性（获取线上最后一个文件的ID），无解。执行本方法之后，就会把线上其他业务的附件给删除掉。 // 如果非要运行，那就把这个判断注释掉。但一定不能在本地运行deleteOldAttachments方法，这样就不会删掉线上的文件数据及记录。 // 测试…

2025年泛微OA专用接口数据入库更新工具类-多主键配置（原创）
type: item
date: 2025-06-19T11:05:00+08:00
description: 这个工具类是：2025年泛微OA专用接口数据入库工具类（原创） 的升级版，其实也不能算是升级版，主要是应对不同的业务。之前的是数据如果存在则直接跳过，这个版本是数据不存在新增，存在则按照主键更新，并且支持多个主键。直接上代码：package com.api.zp.interfaces.util; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import weaver.conn.RecordSet; import weaver.general.BaseBean; import java.util.*; import java.util.stream.Collectors; /** * @Author zF. * @ClassName JsonToDbUpdateUtil.java * @ProjectName 通用json数据入库/更新处理工具类 */ public class JsonToDbUpdateUtil { private static BaseBean base = new BaseBean(); /** * 按数据库字段顺序将JSONObject写入或更新指定表 * @param rs 数据库连接 * @param tableName 目标数据库表名 * @param json 待写入的JSON对象 * @param customJson 自定义JSON参数，优先级高于原始json * @param uniqueFields 唯一字段列表，数据去重使用，避免重复落库 */ public static void upsertByDbFieldOrder(RecordSet rs, String tableName, JSONObject json, JSONObject customJson, List<String> uniqueFields) { // 合并原始JSON与自定义JSON（自定义优先级更高） JSONObject mergedJson = new JSONObject(json); // 把json的所有key都转换成大写 JSONObject convertedJson = convertKeysToUpperCase(mergedJson); if(customJson != null) { convertedJson.putAll(customJson); } // 转换唯一字段为大写 List<String> upperUniqueFields = uniqueFields.stream() .map(String::toUpperCase) .collect(Collectors.toList()); // 验证所有唯一字段在JSON中存在 for (String field : upperUniqueFields) { if (!convertedJson.containsKey(field)) { base.writeLog("错误：唯一字段[" + field + "]在JSON中不存在，操作终止！"); return; } } // 构建WHERE条件 StringBuilder whereClause = new StringBuilder(); List<Object> whereParams = new ArrayList<>(); for (String field : upperUniqueFields) { if (whereClause.length() > 0) { whereClause.append(" AND "); } whereClause.append(field).append(" = ?"); whereParams.add(convertedJson.get(field)); } // 检查记录是否存在 String checkSql = "SELECT COUNT(1) AS CNT FROM " + tableName + " WHERE " + whereClause; boolean exists = false; try { rs.executeQuery(checkSql, whereParams.toArray()); if (rs.next()) { exists = rs.getInt("CNT") > 0; } } catch (Exception e) { base.writeLog("检查记录存在性失败！表[" + tableName + …

2025年泛微OA专用接口数据入库工具类（原创）
type: item
date: 2025-06-05T17:50:00+08:00
description: 最近刚刚接手了泛微OA的项目，项目代码简直是一座庞大的屎山。不得不说，习惯了Spring的生态以后，再去适应其他的框架简直是一种折磨。尤其是这种连JavaBean都不用的项目，所有的参数处理全都是用json和map进行接收和响应……最让我难以忍受的是，所有和数据库交互的地方全都是在Java代码里面直接嵌入的，代码读起来太阳穴突突跳动，咖啡在胃里翻涌成酸。所以被逼无奈，只能亲手封装了一个工具类，简直是优雅极了！[scode type="blue"]20250609 补充：这两天本地跑项目老是自动终止，通过debug发现是方法在再次new RecordSet()的时候会突然结束掉，个人推断是数据库连接池满了。网上搜了一下：数据库操作类 RecordSet 能复用该类进行查询、插入、更改操作，尽量选择复用。尽量选择少建立新的 RecordSet 类进行数据库操作。所以修改了一下工具类，减少不必要的资源消耗，把能从上游传入的都改成传入。再次启动，即解决了这个问题。[/scode]package com.api.zp.interfaces.util; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import weaver.conn.RecordSet; import weaver.general.BaseBean; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Vector; /** * @Author zF. * @ClassName JsonToDbUtil.java * @ProjectName 通用json数据入库处理工具类 */ public class JsonToDbUtil { private static BaseBean base = new BaseBean(); /** * 按数据库字段顺序将JSONObject写入指定表 * @param rs 数据库连接 * @param tableName 目标数据库表名 * @param json 待写入的JSON对象 * @param customJson 增自定义JSON参数，优先级高于原始json * @param uniqueField 唯一字段，数据去重使用，避免重复落库 */ public static void insertByDbFieldOrder(RecordSet rs, String tableName, JSONObject json, JSONObject customJson, String uniqueField) { // 获取数据库字段列表（按表定义顺序） // 合并原始JSON与自定义JSON（自定义优先级更高） JSONObject mergedJson = new JSONObject(json); // 把json的所有key都转换成大写 JSONObject convertedJson = convertKeysToUpperCase(mergedJson); if(customJson != null) { convertedJson.putAll(customJson); // 自定义JSON的键会覆盖原始JSON的相同键 } // 获取唯一字段值（转换为大写匹配数据库字段） String upperUniqueField = uniqueField.toUpperCase(); Object uniqueValue = convertedJson.get(upperUniqueField); if(uniqueValue == null) { base.writeLog("错误：唯一字段[" + upperUniqueField + "]在JSON中不存在，插入终止！"); return; } // 检查唯一字段是否已存在 String checkSql = "SELECT " + upperUniqueField + " FROM " + tableName + " WHERE " + upperUniqueField + " IS NOT NULL"; try { rs.execute(checkSql); // 数据库中是否已经存在当的json Vector array = rs.getArray(); List<String> newMergeList = mergeList(arr…

在已经定义MysqlDataSourceConfig并使用druid的情况下实现多数据源配置
type: item
date: 2025-04-09T09:37:00+08:00
description: 昨天，同事拿到一个需求，要在已有的配置上新增一个SQLserver数据库，实现多数据源。找到我的时候，我寻思这不有手就行嘛？直接用mybatis-plus的dynamic-datasource就好了啊。然后通过@DS注解在mapper接口上面指定对应的数据库就……结果很打脸，按照mybatis-plus官方的配置指定默认数据库以后一直报错：Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class Action: Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular profile you may need to activate it (the profiles db,redis,mq,config,sap,pre are currently active). 进程已结束，退出代码为 1不得不说，这还是我职业生涯中第一次遇到这种问题。我首先考虑到的是：会不会是阿里巴巴的连接池配置影响到的？所以我尝试把阿里巴巴的连接池配置迁移到MySQL数据库的配置下面，还是报错。网上搜了一堆教程，给新的SQLserver也加了连接池配置，依旧无效。可能是驱动没有生效？把SQLserver的驱动换成jTDS试试……就这么折腾了一个多小时，期间我还使用了最近非常火的trae，希望它可以解决我的燃眉之急。结果它在获取了项目文件列表和报错信息的情况下还是一通乱答，排查只能是靠自己了……毕竟帮人帮到底嘛，虽然这不是我自己的工作~就在我最终打算放弃的时候，突然想到会不会是优先级的问题，可能在某个地方定义了一个配置，导致项目启动的时候默认走到了那个配置文件，导致我们后面新增的配置没有被扫描到……就这样从下午五点多折腾到了晚上八点半，除去中间吃饭的半个小时。终于有点头绪了。。。就是这个配置类，项目里面定义了一个MysqlDataSourceConfig：package cn.com.demo.infras.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org…

JAVA快速开发微信机器人-新星框架（NewBot-Nova）
type: item
date: 2024-10-17T20:10:55+08:00
description: 这个框架其实最早从2023年初就开始写了，以前纯粹是自己用的，所有的东西都一股脑的揉在一起。后来在群里聊天，群友希望我能把东西共享出来。本来是想直接发的，但是第一写的太乱了，其次也不容易理解。于是趁着不上班，从上个月中旬开始准备重构一下。这段时间平均每天写十六个小时，毛都要写秃了。当然也遇到了很多问题，什么奇葩问题都遇到了。也用到了一些新东西，比如框架的开发加配套工具一共用到了：Java、易语言、bat、PHP、vue……算起来这是我第一次自己去做一个完整的开源方案。文档地址：https://10ke.cn/ （有兴趣的可以看一下）这本来是我离职前计划的一部分，完成了这件事后其他的事也得陆续提上日程了。

SpringBoot生成项目文件目录树
type: item
date: 2024-10-10T20:23:00+08:00
description: 在很多项目的开发文档上面我们都能看到很漂亮的目录树。如果纯手敲的话那得需要很久，可能还不标准。最近还是在写机器人框架，涉及到插件开发部分的说明，需要向用户展示插件的结构。手敲目录树太麻烦了，于是我搞了个工具类：package com.chaizhou; /** * @Author zF. * @ClassName ProjectStructureGenerator.java * @ProjectName NewBot-FrameWork * @Description 项目结构生成器 */ import java.io.File; import java.io.FileWriter; import java.io.IOException; public class ProjectStructureGenerator { public static void main(String[] args) { File rootDir = new File("src"); try { FileWriter writer = new FileWriter("project_structure.txt"); generateProjectStructure(rootDir, writer, 0); writer.close(); System.out.println("Project structure file created successfully."); } catch (IOException e) { e.printStackTrace(); } } private static void generateProjectStructure(File dir, FileWriter writer, int level) throws IOException { if (dir.isDirectory()) { File[] files = dir.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { File file = files[i]; String prefix = (i == files.length - 1) ? "└── " : "├── "; writer.write(getIndentation(level) + prefix + file.getName() + "\n"); if (file.isDirectory()) { generateProjectStructure(file, writer, level + 1); } } } } } private static String getIndentation(int level) { StringBuilder indentation = new StringBuilder(); for (int i = 0; i < level; i++) { indentation.append(" "); } return indentation.toString(); } } 使用方法，直接把上面的工具类放在项目的java文件夹下面，然后运行main方法即可。文件会生成在项目根目录下，效果：└── main ├── java └── com └── chaizhou ├── event ├── EventAbstract.java └── RequestResultVO.java ├── plugin ├── PluginStatus.java └── QueryPhonePlugin.java └── resources怎么样？是不是很标准？

mvn install -Dmaven.test.skip=true
type: item
date: 2024-10-09T02:15:00+08:00
description: 这个命令用于使用 Maven 构建和安装项目，同时跳过测试阶段。具体来说：命令解析mvn install：功能：编译项目、运行测试、打包（如生成 JAR 或 WAR 文件），然后将生成的包安装到本地 Maven 仓库中。这样，其他本地项目可以引用这个包作为依赖。生命周期阶段：install 是 Maven 生命周期中的一个阶段，位于 package 之后，确保项目的构建产物被正确安装到本地仓库。-Dmaven.test.skip=true：功能：通过设置 Maven 的属性 maven.test.skip 为 true，指示 Maven 在构建过程中跳过运行测试代码。这意味着测试编译和执行步骤都会被跳过。使用场景：当你确定代码已经经过测试，或者在某些情况下（如快速构建、测试不相关的改动等）希望节省时间时，可以使用这个参数。综合作用整个命令 mvn install -Dmaven.test.skip=true 的作用是：编译项目：将源代码编译成可执行的字节码。打包：根据项目配置生成相应的包（如 JAR、WAR）。安装：将生成的包安装到本地 Maven 仓库，供其他本地项目引用。跳过测试：在上述过程中不执行任何测试相关的步骤，节省构建时间。注意事项跳过测试的风险：虽然跳过测试可以加快构建速度，但可能会导致未发现的代码问题被忽略。因此，建议在确保代码稳定性和测试通过的情况下使用。示例mvn install -Dmaven.test.skip=true上述命令会执行项目的编译、打包和安装步骤，但不会运行任何测试。总结使用 mvn install -Dmaven.test.skip=true 可以快速完成项目的构建和安装过程，适用于需要快速部署或在确认代码已经通过测试的情况下。然而，为了保证代码质量，建议在开发过程中尽量执行测试，只有在特定需求下才跳过测试步骤。

SpringBoot注入service为null（空指针异常）解决方案
type: item
date: 2024-10-07T01:19:00+08:00
description: 最近在写微信机器人框架，经过一周左右的时间，终于搞定了框架的插件开发模式（独立创建插件项目、热拔插、解耦合）。但是在导入service的时候出现了问题，正常来说直接通过@Autowired即可导入我们需要的service接口。 @Autowired private PluginOperate pluginOperate; // 自动注入插件管理器但是运行插件的时候出现了空指针异常，排查了很久，包括问GPT。然后网上搜了几个小时，一度想要放弃……好在最后终于是解决了。[scode type="blue"]出现的原因： Spring管理的都是单例（singleton），和多对象相冲突。框架项目启动时初始化，会初始化插件管理器，Spring同时会为其注入service，该对象的service不是null，被成功注入。但是，由于Spring默认管理的是单例，所以只会注入一次service。当插件载入框架并启动，框架端会加载插件提供的服务，但是Spring管理的都是单例，不会给新的对象注入service，所以导致只要是创建新的对象，都不能再注入了。像controller里面有service，service里面有dao。因为controller，service，dao都有是单例，所以注入时不会报null。但是插件不是单例，所以使用Spring注入一次后，后面的对象就不会再注入了，会报NullException。[/scode]解决方案： private static PluginOperate pluginOperate; @Autowired public void setPluginOperate(PluginOperate pluginOperate) { SystemPlugin.pluginOperate = pluginOperate; }此时即可在插件中调pluginOperate提供的方法，而不再报空指针异常。