
简介JAVA 充电桩协议库JCPP配套完整开源充电平台源码面向充电桩运营平台开发者、协议对接工程师及物联网学习者解决多厂商充电桩协议适配、互联互通、多租户与分时计费等核心问题。包内包含 586 个文件整合 SpringCloud、MySQL、Netty 与 uniapp 技术栈尤其以 468 个 Java 源文件为主配合 XML/YML/Properties 配置、Proto 协议定义、TSX 前端组件及 Dockerfile、SQL 脚本覆盖后端服务、协议解析、管理后台、小程序和模拟桩完整链路。压缩包大小约 1.06MB目录结构清晰适合快速搭建环境并二次开发。已有 72 人学习适合希望落地充电桩云平台并深入理解云快充 1.5/1.6、南网 104、互联互通等多协议细节的中高级开发者。1. 充电桩协议库JAVA实现为什么说JCPP是绕不开的集成层做充电运营平台的朋友应该都有过这种经历平台本身跑得挺稳但接充电桩时被协议折腾到怀疑人生。云快充一套报文南网104又是另一套京能、绿能、挚达、星星、领充、EN各自为政每个桩企的文档风格还不一样有的给完整示例有的只甩过来一个 PDF 让你自己对着抓包。这个 JAVA 充电桩协议库 JCPPJava Charging Pile Protocol就是干这个事的——把国内主流的充电桩协议统一封装成一套可调用的接口让上层业务不用关心底层报文差异。它对充电协议做了抽象屏蔽了各家桩企的私有字段、时序和鉴权方式适合正在做充电平台、桩企管理后台或者新能源运营系统的 Java 工程师直接拿来用。2. 协议库的技术底座JCPP 的模块划分与选型理由2.1 协议解析层的设计思路状态机 报文适配JCPP 这类协议库最核心的不是通信而是报文解析。充电桩和平台之间走的是 TCP 长连接报文格式从十六进制到 JSON 都有而且同一个协议里还存在版本差异。JCPP 的做法是典型的协议适配器 状态机两层结构适配器负责把桩端发来的原始字节流转成内部统一的事件模型状态机负责跟踪每把充电枪当前处于什么阶段——空闲、插枪、鉴权、充电中、结束。这样做的好处是上层业务不用关心桩端到底什么时候会主动上报状态。你只需要监听充电状态变更事件剩下的事情状态机帮你兜住。比如云快充协议里桩端可能会在充电过程中多次推送实时数据每次推送的字段还不完全一样有电压电流、有SOC、有累计电量。如果这些数据直接抛给上层业务方就得自己拼状态很容易漏算。JCPP 把同一充电订单的多次上报合并成一个连续的状态流业务层拿到的永远是当前订单的最新完整快照。// 协议层处理入口统一接收桩端上报的原始报文 public class ProtocolDispatcher { private final MapString, ProtocolAdapter adapters; public void onReceive(String pileCode, byte[] rawData) { // 根据桩编码找到对应协议适配器如 CloudQuickAdapter / SouthNet104Adapter ProtocolAdapter adapter adapters.get(pileCode); // 适配器将原始报文解析为统一的 ChargeEvent ChargeEvent event adapter.decode(rawData); // 事件交给状态机由状态机决定是否更新订单快照 stateMachine.handle(pileCode, event); } }这段代码是整个协议库的入口逻辑pileCode是桩的唯一编码它决定了走哪个适配器decode把不同协议的报文转成统一的ChargeEvent状态机只认事件不认协议。也就是说不管底层是十六进制帧还是 JSON到了状态机这一层全是同一种对象。关键参数是pileCode到适配器的映射关系这个映射一定要确保每个桩编码只对应一个协议否则同一个桩同时被两个适配器解析状态会直接乱掉。2.2 七种协议栈的差异云快充、南网104等如何共存JCPP 支持云快充、南网104、京能、绿能、挚达、星星、领充、EN 这些协议但它们的差异不是一个模子里刻出来的。云快充是国内运营平台覆盖率最高的协议报文偏 JSON字段命名和国标接近文档也相对规范适合做主协议。南网104 是偏电力系统风格的协议报文里有大量 BCD 编码的小数字段像 BCD 码的电量、BCD 的电压解析时得注意字节序和压缩格式。京能和绿能的协议介于两者之间走的也是 TCP 长连接但心跳和鉴权时序有各自的规定。挚达、星星、领充、EN 这些桩企协议更多是私有协议有的基于 Modbus 变种有的在 TCP 层直接定义了私有帧头。JCPP 的设计原则是协议隔离、数据统一——每种协议一个包互不依赖编译期就避免互相污染。这样当你只需要接云快充时可以只引入对应模块不需要把整个协议库全量加载。实际集成时JCPP 暴露的接口是相同的区别只在配置上# jcpp-config.properties # 云快充协议配置 jcpp.protocol.cloudquick.pile-prefixCQ jcpp.protocol.cloudquick.host127.0.0.1 jcpp.protocol.cloudquick.port8300 # 南网104协议配置 jcpp.protocol.southnet104.pile-prefixSN jcpp.protocol.southnet104.host127.0.0.1 jcpp.protocol.southnet104.port8400pile-prefix是桩编码前缀框架通过前缀快速路由到对应协议栈。host和port是平台侧开启的监听端口桩端主动连过来。注意这里的方向常规情况下是桩主动连平台所以平台侧是个 TCP Server而不是去连桩。很多新手第一次接协议时默认平台是客户端容易把方向搞反。如果一个平台同时接多个协议就把每个协议的监听端口分开避免端口冲突。2.3 加桩流程中 JCPP 扮演的角色运营平台加一个新型号的桩时最怕的是桩上线了但数据全乱。JCPP 把加桩流程拆成了三个可验证的节点通道注册、心跳保持、业务上报。通道注册阶段会校验桩编码和协议类型是否匹配不匹配直接拒绝连接而不是等到后面解析报文时抛异常。心跳保持阶段由 JCPP 内部定时器驱动不需要业务层参与如果协议要求 30 秒一次心跳你只需要在配置里写好间隔即可。业务上报阶段是真正处理充电数据的地方。JCPP 会把实时数据、账单数据、故障数据分开路由各自对应一个事件回调。这样平台侧的架构就可以按业务域拆分处理逻辑不需要一个类里堆几百个 if-else。所以在团队里后端同学只需要关注事件回调里的业务实现协议细节全部由 JCPP 消化。3. 落地集成把 JCPP 跑起来的完整操作3.1 依赖引入与初始化的顺序拿到 JCPP 源码包后不要急着改业务代码先把协议库作为一个独立模块编进项目。它是标准的 Maven 工程目录结构里协议实现都在src/main/java下跟着包名走resources下是协议模板和配置样例。如果只是做桩端协议适配建议把它打成 jar 引入现有 Spring Boot 或 SSM 项目避免源码直接混进业务工程里。# 先编协议库本体跳过测试减少干扰 mvn clean install -DskipTests # 编译产物会生成在 target/jcpp-core-x.x.x.jar这里有个习惯问题我一般会先把协议库单独装到本地仓库业务工程通过坐标依赖引用。这样协议库升级时业务代码不用重新编译只要换 jar 版本即可。-DskipTests要保留协议库的测试依赖模拟桩端环境本地没有模拟器时跑测试很容易报连接超时跟代码质量没有关系。依赖引入后初始化顺序很关键。先初始化协议路由表再启动 TCP 监听最后再执行业务监听器注册。顺序反了会出现在监听器还没就绪时桩端已经连上来并推送数据导致部分事件丢失。JCPP 内部有事件缓冲队列但队列有上限满时会丢弃最旧的事件所以初始化顺序不要省。3.2 协议通道绑定与桩端握手桩端上线第一个动作是发送握手报文附上桩编码和协议版本号。JCPP 拿到这个报文后做两件事核对桩编码前缀是否存在于路由表然后回发握手确认帧。这个握手过程不是简单的 TCP accept而是协议层业务握手——没通过校验的连接会被直接关闭即使 TCP 层面还通着。// 通道启动示例注册监听端口并绑定协议 public void startGateway() { JcppGateway gateway new JcppGateway(); // 端口 8300 跑云快充协议 gateway.registerListener(8300, CLOUD_QUICK, (channel, event) - { if (event.getType() EventType.CHARGING_PUSH) { // 实时电量上报这里更新充电中订单 handleChargingData(event.getPileCode(), event.getPayload()); } }); // 端口 8400 跑南网104协议 gateway.registerListener(8400, SOUTH_NET_104, (channel, event) - { if (event.getType() EventType.ORDER_FINISH) { // 订单结束这里结算账单 settleOrder(event.getPileCode(), event.getPayload()); } }); gateway.start(); }这段代码展示了 JCPP 最典型的用法registerListener把端口和协议绑定第二个参数是协议类型第三个参数是事件回调。EventType.CHARGING_PUSH表示充电过程中的实时数据推送EventType.ORDER_FINISH表示充电订单结束。业务层只需要判断事件类型不需要知道报文长什么样。参数说明如果同一协议需要部署多个端口比如按区域拆分每秒新增的registerListener调用会创建独立的协议处理器实例彼此状态互不影响。3.3 一个下单充电的最小实现充电业务里最常用的操作是远程启动充电。对应到 JCPP 里就是向下发一条启动充电指令并等待桩端返回确认。不同协议的指令帧格式不同但经过 JCPP 封装后调用方式收敛为一个方法public void startCharge(String pileCode, String orderId, int gunNo, int feePolicyId) { StartChargeRequest request new StartChargeRequest(); request.setOrderId(orderId); request.setGunNo(gunNo); request.setFeePolicyId(feePolicyId); // 协议库根据 pileCode 自动选用对应协议的启动帧格式 CommandResult result jcppGateway.sendStartChargeCommand(pileCode, request); if (result.isSuccess()) { // 启动指令被桩端确认订单状态转为充电中 orderService.markCharging(orderId); } else { // 桩端拒绝或超时需要记录原因码 orderService.markFailed(orderId, result.getErrorCode()); } }这里值得留意的是sendStartChargeCommand的返回值只代表桩端确认收到指令并不代表枪已经开始充电。云快充协议里平台下发启动后桩端可能延迟几秒才真正吸合继电器所以更稳的做法是监听后续的充电状态推送事件以推送事件作为订单状态流转的最终依据。feePolicyId是计费策略编号不同桩群可能维护不同的费率模板这个参数要确保在桩端已存在否则桩端会返回计费策略不存在而拒绝启动。4. 协议参数对照云快充、南网104等充电桩协议报文要点拿到协议库后第一件事不是写代码而是先建立一张参数对照表。不同协议的心跳间隔、报文头格式、计费精度差别很大把这些参数摸清楚后面排查问题会快很多。协议名称报文格式心跳间隔计费字段精度离线判定时间云快充JSONUTF-830 秒小数点后 2 位90 秒南网104BCD 编码十六进制帧60 秒小数点后 4 位180 秒京能私有文本帧30 秒小数点后 2 位120 秒绿能JSONUTF-830 秒小数点后 2 位120 秒挚达私有十六进制帧15 秒小数点后 3 位60 秒星星JSON 签名30 秒小数点后 2 位90 秒领充私有文本帧60 秒小数点后 2 位180 秒ENJSONUTF-830 秒小数点后 3 位120 秒这张表是我从 JCPP 源码的协议模板里整理的不同版本可能略有出入。重点看两个字段心跳间隔和计费字段精度。心跳间隔直接决定桩的离线判定时间如果平台侧设置的心跳超时太短桩端偶尔网络抖动就会被误判离线。计费字段精度是账目的关键南网104 的 4 位小数精度如果按 2 位去解析长期跑下来账单金额会出偏差。JCPP 在解析时已经按各协议原生的精度处理了但如果你在业务层二次计算时用了double小数的精度损失会被放大这一点下文避坑部分会再提。报文格式差异直接关系到传输层的解析方式。云快充的 JSON 报文可以直接用 Jackson 映射对象但南网104 的 BCD 编码需要先把二进制帧转成字符串再按字段长度切分。JCPP 已经把这一层封装好了但你排查问题时要知道这个背景看到日志里出现乱码一样的十六进制内容那不是日志编码坏了而是 BCD 报文的原始形态。5. 避坑指南充电桩协议库 JAVA 集成常见问题与排查5.1 现象桩端持续连接又断开日志反复出现“握手失败”原因桩编码前缀没有正确映射到协议类型。JCPP 的路由规则是按pile-prefix匹配如果配置里写了CQ前缀但实际桩编码以CZ开头协议库无法识别直接拒绝握手。解决核对桩端实际编码的前几位字符统一修改配置。另外注意有些桩企允许自定义桩编码前缀如果现场桩编码被人为改过要和桩端运维确认后用新前缀配置而不是凭文档猜测。5.2 现象订单结算金额与桩端屏幕显示不一致差几分钱原因典型的计费精度丢失。以云快充为例协议里电量单位是“度”精度到小数点后 2 位但部分平台在业务层为了计算方便先把电量转成double再乘电价double在乘除中会引入无规律的精度误差。解决业务层统一用BigDecimal并且明确 scale。电量、电价、服务费全部按协议定义的小数位截取乘完后再截取一次不要依赖数据库的浮点字段去约等。JCPP 解析出的原始电量是字符串或BigDecimal不要为了图省事转成double再处理。5.3 现象充电过程中实时数据推送会偶发丢一条但桩端显示正常原因事件队列积压。JCPP 内部有有界事件队列当业务回调处理速度跟不上桩端上报频率时队列满了会丢弃新事件。尤其是云快充这类 30 秒推送一次的协议如果业务回调里做了数据库写操作且没有超时控制很容易积压。解决回调里不要做重活数据落库走异步线程池回调只负责把数据扔进线程池就返回。队列大小可以在配置里调大但根因是消费速度不够异步化才是正解。5.4 现象南网104协议下电压电流解析出来数值偏大十倍或百倍原因BCD 编码解析时字节序错位。南网104 报文里电压字段用的是压缩 BCD 码一个字节存两位数字比如实际电压 220.5V报文里存的是22 05两个字节。如果按普通十六进制直接转整数会得到 0x2205 也就是 8709自然偏大。解决JCPP 内部已处理了 BCD 转换但如果你在业务层再次对原始报文做校验不要用现成的十六进制字符串转整数。直接引用 JCPP 解析后的字段对象或者通过工具方法BcdUtil.toInt(byte[])转换。5.5 现象多把枪同时充电时订单数据互相串原因状态机的 key 没有细化到枪号。一个桩通常有多把枪如果协议库里状态机的 key 只用了桩编码那同一桩下两把枪的充电状态会互相覆盖。解决确认 JCPP 版本中状态机的 key 是否包含枪号。如果源码里只用了pileCode改成pileCode : gunNo。我遇到过不少二次开发者在协议库上做定制只改了报文解析漏了这个状态隔离导致并发充电时订单混乱这是最容易翻车的点之一。6. 从能用跑到好用调试技巧与扩展新协议先把一个最实用的调试手段拿出来JCPP 的日志默认没有开关控制你要在 Logback 里单独给协议包开 DEBUG 级别这样才能看到原始报文。别小看这一步排查问题的时候没有原始报文等于盲人摸象。!-- logback.xml 中单独开启协议库的报文日志 -- logger namecom.jcpp.protocol.codec levelDEBUG/ logger namecom.jcpp.transport.handler levelDEBUG/开 DEBUG 之后日志里会输出收发帧的完整内容。但注意DEBUG 级别的报文日志会打印整个字节数组如果有一把枪正在充电数据量会很大建议只在预发环境或者测试桩上打开不要在生产环境全天开着。验证协议库是否正常工作最直接的方法是模拟桩端回包。把 JCPP 的监听端口视为平台侧服务你可以在本地写一个测试脚本模拟桩端连接逐条发送协议样例中的充电报文观察 JCPP 事件回调是否被正确触发。# 用 nc 模拟桩端发送一条云快充心跳报文十六进制帧样例 printf {type:heartbeat,pile_code:CQ000001,ts:2025-05-20 10:00:00} | nc -q 5 127.0.0.1 8300这里要强调的是nc发送的是样例报文实际业务中被 JCPP 封装的握手报文不会这么简单。所以我一般更推荐直接用协议库自带的模拟器包如果源码包里没有就整理日志里抓到的真实桩端报文做成离线回放脚本每次改完代码都用同一组报文跑回归。这样改协议库不会引入新的解析回归。扩展新协议时不要从零写适配器。先从现有协议里复制一个结构最接近的适配器比如新增一个和云快充报文风格类似的桩企协议就复制CloudQuickAdapter改报文类型映射和必填字段校验然后跑一遍回放脚本。JCPP 的适配器接口是关键decode是入口encode是出口指令下发路径改encode上报解析路径改decode两边独立验证。从那以后我每次接新协议都会先走一遍这个流程先抄最接近的适配器改完立刻用报文回放做回归最后再写业务对接。这套流程救过我很多次尤其是南网104 那种 BCD 编码的协议不靠回放脚本很难在一个下午内把字段全部对齐。希望帮到你。本文还有配套的精品资源点击获取