
简介这份Java HTTP生成微信小程序二维码的源码包聚焦裂变分享、渠道推广和红包奖励等常见运营场景为每位账号生成专属且永久的邀请二维码面向需要实现邀请码裂变、渠道追踪和奖励规则的Java后端开发者。项目基于微信官方getUnlimitedQRCode接口采用前端→后端API→微信API的安全调用链路妥善处理secret/token等敏感信息并涵盖5种生成实现方式。资源为压缩包形式共69个文件包括XML配置、JAVA源码、Properties配置、Class编译文件及少量JAR依赖整体仅53KB目录结构清晰包含pom.xml、源码目录与测试目录适合Maven工程快速对照或直接引入。内容完整给出各种实现下的工具类、接口封装、参数组装与异常处理模块附带可运行的测试代码便于开发者理解官方接口调起细节并复用至自己项目中。当前已有2646人学习/下载适合具备基础Java和HTTP知识、想在小程序场景中落地二维码功能的读者。1. 微信小程序二维码五种 Java HTTP 实现方式一眼看清坑在哪做小程序裂变和渠道追踪时生成小程序码是后端最常被提的需求之一。很多人第一反应是拿 ZXing 之类库生成一张普通二维码结果用户扫开是网页或小程序首页携带的渠道参数根本没进去等于白做。真正能落地的做法是后端用 Java 发起 HTTP 请求调微信官方接口拿带 scene 参数的小程序码图片字节流。这份资源把常见路径全部走了一遍整理出五种实现方式——从 JDK 原生 HttpURLConnection、Apache HttpClient、OkHttp 到 Spring RestTemplate再到 WxJava SDK每段代码都能直接改着用参数和返回结构也都拆开讲清楚了。适合有 Java 基础、准备在渠道追踪和分享拉新场景里自己做二维码生成服务的开发者新手也能照着把码生成出来。2. 小程序码生成原理把 access_token 与 wxacode 接口一次吃透2.1 小程序码与普通二维码的本质差异普通二维码是把一段字符串编码成黑白方块扫码后由微信内置浏览器解析打开的是链接或者网页。小程序码走的是完全不同的协议链路后端拿到 access_token 后调用wxacode.getUnlimited接口把 scene 参数传给微信服务器微信会签发一张带私有格式的图片字节流扫码后直接进入小程序指定页面scene 里的渠道标识会被注入到页面参数中。这两者的区别决定了实现方式完全不同。普通二维码生成是纯本地计算不依赖网络小程序码生成必须依赖 HTTP 请求微信服务器而且对请求频率、参数格式有严格限制。最常见的翻车姿势是后端把小程序页面链接丢给 ZXing 转成二维码然后兴高采烈地交付结果现场扫码直接打开一个不存在的 webview用户一脸懵需求方也一脸懵。所以在写任何 HTTP 请求代码之前先把接口协议弄清楚比什么都重要。2.2 获取 access_token先做这一步生成小程序码的接口都要求传 access_token这个 token 通过 appid 和 secret 换取有效期 7200 秒。每次生成码都重新获取一次 token 不仅慢还容易触发官方的调用频率限制所以实际工程里必须先封装一个带缓存能力的 access_token 获取方法。// 常见做法是用 URL 直接拼接但 appid 和 secret 都建议做 URL 编码 String url https://api.weixin.qq.com/cgi-bin/token ?grant_typeclient_credential appid URLEncoder.encode(appid, UTF-8) secret URLEncoder.encode(secret, UTF-8); // 这里用 HttpURLConnection 做演示生产环境建议换成连接池 HttpURLConnection conn (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod(GET); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); String response readResponse(conn.getInputStream()); JSONObject json JSON.parseObject(response); String accessToken json.getString(access_token); // 如果 errcode 存在说明 appid、secret 配置有误直接结束这段代码的逻辑核心就是一次标准 GET 请求。grant_type固定为client_credential这是微信 OAuth 体系里客户端凭证模式的固定值appid是小程序后台的 AppIDsecret是 AppSecret两个值拼接前最好先 URL 编码否则遇到特殊字符会直接 401。拿到返回的 JSON 后只取access_token字段同时关注expires_in字段确认有效期。拿到 token 后一定要缓存。我一般用 Redis 存一份key 用小程序的 appidvalue 放 token过期时间设置为 7000 秒而不是 7200 秒提前 200 秒刷新避免在到达有效期边界时请求失败。缓存里还要处理并发刷新问题下面避坑章节会详细说。2.3 wxacode.getUnlimited参数表与格式说明这个接口就是生成小程序码的核心入口完整路径是https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_tokenACCESS_TOKEN请求方式是 POST请求体是 JSON。参数类型必填说明sceneString是最长 32 个可见字符只支持数字、大小写英文与部分特殊字符pageString否小程序页面路径不需要以/开头不填默认跳到主页widthInteger否二维码宽度默认 430px最大 1280pxauto_colorBoolean否自动配色默认 false打开后 line_color 失效line_colorObject否线条颜色RGB 格式auto_color 为 false 时才生效is_hyalineBoolean否是否需要透明底色默认 false这接口最坑的地方在于返回值格式不固定。正常情况下返回的是image/jpeg二进制图片流一旦参数有问题返回的就是 JSON 错误码消息。这导致代码逻辑必须同时处理两种返回值直接用response.body().string()一把梭接图片流必然翻车。正确做法是先读字节流判断前几个字节是不是 JPEG 文件头FF D8 FF是就说明拿到图了不是就说明拿到的是错误信息。3. 三种自实现方案HttpURLConnection、HttpClient、OkHttp 逐行拆3.1 方式一JDK 原生 HttpURLConnection零依赖的兜底方案如果项目里没有引入任何 HTTP 客户端库或者在某些受限环境里不能加依赖JDK 自带的 HttpURLConnection 也能完成整个流程。它的优势是完全不需要引入第三方包缺点是连接复用和编码控制比较原始需要自己小心处理。public byte[] createWxaCodeUnlimit(String accessToken, String scene, String page) throws IOException { String url https://api.weixin.qq.com/wxa/getwxacodeunlimit ?access_token URLEncoder.encode(accessToken, UTF-8); HttpURLConnection conn (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod(POST); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setDoOutput(true); conn.setUseCaches(false); conn.setRequestProperty(Content-Type, application/json); JSONObject body new JSONObject(); body.put(scene, scene); body.put(page, page); body.put(width, 430); body.put(auto_color, false); body.put(line_color, new JSONObject().put(r, 0).put(g, 0).put(b, 0)); try (OutputStream os conn.getOutputStream()) { os.write(body.toJSONString().getBytes(StandardCharsets.UTF_8)); } int status conn.getResponseCode(); if (status ! 200) { throw new IOException(HTTP status: status); } return readAllBytes(conn.getInputStream()); } private byte[] readAllBytes(InputStream in) throws IOException { try (ByteArrayOutputStream out new ByteArrayOutputStream()) { byte[] buffer new byte[4096]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } return out.toByteArray(); } }这段代码有几个容易忽略的细节。setDoOutput(true)是必须的否则getOutputStream()会抛异常setUseCaches(false)防止 JDK 对 POST 请求做缓存导致拿到过期数据。line_color参数是个嵌套 JSON 对象里面必须包含r、g、b三个字段颜色值范围是 0 到 255为什么不建议用auto_colortrue后面避坑章节会说。这套方案的边界也很明确并发量不高、单机部署、对依赖敏感的场景够用如果服务要横向扩容多个实例同时用 HttpURLConnection 调微信接口连接池缺失的短板会很明显。3.2 方式二Apache HttpClient传统项目的标配老牌 Spring 项目里最常见的就是 Apache HttpClient它弥补了 HttpURLConnection 没有连接池的问题PoolingHttpClientConnectionManager可以把 TCP 连接复用起来减少每次请求的 TCP 握手开销。private final CloseableHttpClient httpClient; public void init() { PoolingHttpClientConnectionManager cm new PoolingHttpClientConnectionManager(); cm.setMaxTotal(200); cm.setDefaultMaxPerRoute(50); httpClient HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(RequestConfig.custom() .setConnectTimeout(5000) .setSocketTimeout(5000) .setConnectionRequestTimeout(3000) .build()) .build(); } public byte[] createWithHttpClient(String accessToken, String scene, String page) throws Exception { String url https://api.weixin.qq.com/wxa/getwxacodeunlimit ?access_token URLEncoder.encode(accessToken, UTF-8); HttpPost post new HttpPost(url); post.setHeader(Content-Type, application/json); JSONObject body new JSONObject(); body.put(scene, scene); body.put(page, page); post.setEntity(new StringEntity(body.toJSONString(), ContentType.APPLICATION_JSON)); try (CloseableHttpResponse resp httpClient.execute(post)) { int status resp.getStatusLine().getStatusCode(); if (status ! 200) { throw new IOException(HTTP status: status); } return EntityUtils.toByteArray(resp.getEntity()); } }这里的连接池参数需要结合服务实际并发量调整。setMaxTotal(200)是连接池总上限setDefaultMaxPerRoute(50)是每个路由一个域名算一个路由的最大连接数微信接口域名就一个所以这两个值可以设为相同。setConnectionRequestTimeout(3000)是从连接池获取连接的最大等待时间超过了直接报错防止线程全部阻塞在连接获取上。注意这段代码里我用的是EntityUtils.toByteArray()不是EntityUtils.toString()。这是最容易踩的坑微信接口返回的是二进制图片流用toString()会把字节做字符串解码图片全毁写入本地文件也会变成损坏的 JPEG。下面避坑章节还会清重点说。3.3 方式三OkHttp响应式流的体验更好OkHttp 在并发场景下的表现更稳builder 模式的配置也直观做拦截器日志和超时控制都方便。它对二进制流的处理也更干净ResponseBody.bytes()直接返回字节数组。private final OkHttpClient okHttpClient; public void init() { okHttpClient new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES)) .build(); } public byte[] createWithOkHttp(String accessToken, String scene, String page) throws IOException { String url https://api.weixin.qq.com/wxa/getwxacodeunlimit ?access_token URLEncoder.encode(accessToken, UTF-8); JSONObject body new JSONObject(); body.put(scene, scene); body.put(page, page); body.put(width, 430); Request request new Request.Builder() .url(url) .post(RequestBody.create( MediaType.parse(application/json; charsetutf-8), body.toJSONString())) .build(); try (Response resp okHttpClient.newCall(request).execute()) { if (!resp.isSuccessful()) { throw new IOException(Unexpected code: resp.code()); } return resp.body().bytes(); } }OkHttp 的连接池默认是空闲 5 分钟回收我这里显式配了ConnectionPool(50, 5, TimeUnit.MINUTES)最大 50 个连接。这里的 50 也是按单机并发量扼要估计的值生产要压测后调整。resp.body().bytes()这一步内部会读完整流再返回不需要手动关流try-with-resources已经把 Response 关掉了。OkHttp 相比前两者的优势主要是网络层的可观测性。我习惯在 OkHttpClient 上挂一个日志拦截器把请求耗时、状态码、响应体大小打出来排查问题的时候比对着微信文档猜原因高效得多。4. 两种工程化方案RestTemplate 与 WxJava SDK省一半代码4.1 方式四Spring RestTemplateSpring 项目里直接注入一个 RestTemplate 就能用代码量比前面三种都少。关键在于响应的封装必须用ResponseEntitybyte[]而不是String否则二进制流被转成字符串后没法恢复。Autowired private RestTemplate restTemplate; public byte[] createWithRestTemplate(String accessToken, String scene, String page) { String url https://api.weixin.qq.com/wxa/getwxacodeunlimit ?access_token URLEncoder.encode(accessToken, UTF-8); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); MapString, Object body new HashMap(); body.put(scene, scene); body.put(page, page); body.put(width, 430); HttpEntityMapString, Object requestEntity new HttpEntity(body, headers); ResponseEntitybyte[] response restTemplate.postForEntity(url, requestEntity, byte[].class); if (response.getStatusCode() ! HttpStatus.OK) { throw new IllegalStateException(HTTP status: response.getStatusCode()); } return response.getBody(); }使用 RestTemplate 时要注意两个工程细节。第一注入的 RestTemplate 建议单独建一个 Bean配置连接池而不是直接用默认的new RestTemplate()因为默认实现没有连接池每次请求都新建连接高并发下 TCP 连接数是灾难。第二微信服务器在特殊情况下会返回空的 200 响应体所以拿到response.getBody()后还要再判空。这里用postForEntity而非postForObject原因很简单我需要拿到响应状态码做防御判断而postForObject直接帮你把响应体解析掉遇到错误 JSON 体时会直接抛异常处理路径不够直观。这部分代码在 Spring Boot 项目里能做更简化的封装但底层逻辑不变。4.2 方式五WxJavaweixin-java-miniappSDKWxJava 是目前 Java 生态里微信接口封装最完整的开源 SDK小程序部分是 weixin-java-miniapp。它把 access_token 的获取、缓存、刷新全部封装在内部作为调用方只需要构建请求对象再调一个方法。// 依赖引入com.github.binarywang:weixin-java-miniapp WxMaService wxMaService WxMaServiceManager.getService(); // 实际从 Spring 容器拿 WxMaCodeUnlimitedRequest request WxMaCodeUnlimitedRequest.builder() .scene(inviter10086) .page(pages/home/index) .width(430) .autoColor(false) .lineColor(new WxMaCodeLineColor(0, 0, 0)) .build(); byte[] qrCodeBytes wxMaService.getQrcodeService().createWxaCodeUnlimit(request);这段代码里的WxMaCodeUnlimitedRequest是 SDK 里封装的请求对象builder 模式一眼就能看清楚每个字段含义。createWxaCodeUnlimit方法内部帮我们完成了从缓存取 token、没有就发起获取 token 请求、拼接 URL、构造 POST 请求、判断返回类型、把图片字节流返回。如果接口返回错误SDK 会抛出带 errcode 的异常排查原因时直接看异常 message 里的错误码就行。SDK 也不是没有坑。不同大版本之间的类名和包路径差别很大网上抄来的代码经常用的是旧版本 API理解能力没问题但依赖版本一冲突就原形毕露。建议下载源码包后锁定一个稳定的 Semantic Version 范围不要用latest。4.3 选型对比到底用哪一种实现方式依赖连接复用错误处理接入成本适用场景HttpURLConnectionJDK 自带无手动低受限环境、单次调用Apache HttpClient需引入有手动中传统 Spring 项目OkHttp需引入有手动中高并发、可观测要求高RestTemplateSpring 自带取决于配置手动低Spring Boot 项目WxJava SDK需引入有SDK 封装最低对微信场景集成度高我的选型逻辑很简单如果是 Spring Boot 项目直接上 WxJava SDK因为后续可能还要对接微信支付、订阅消息、手机号快捷登录这些全在同一个 SDK 里省得一套套对接如果不是 Spring 体系首选 OkHttp连接池、超时控制、日志拦截器都成熟。HttpURLConnection 只做兜底方案不折腾。5. 常见问题与避坑生成小程序码最容易翻车的五个点5.1 参数拼装类踩坑41030、40097 错误码现象调用接口返回 JSON{errcode:41030,errmsg:invalid page}或40097。原因90% 是 page 参数带了开头的斜杠。微信文档要求 page 字段不写起始/但很多后端同事习惯性地把前端路由里的/pages/home/index直接塞进来微信服务器识别不了剩下的 10% 是页面路径在 app.json 里根本不存在或者 scene 里塞了中文、空格等不可见字符。解决调用前统一做一次参数清洗。page 以/开头就substring(1)scene 里的中文全部过滤或转成短码scene 超过 32 位就在服务端做压缩映射存一张渠道表和短码的对应关系不要赌前端传过来的字段能刚好合规。5.2 响应处理类踩坑图片字节被当文本解析现象生成的 code 写入本地文件后打不开文件大小只有几百字节或者拿到的是一个包含 JSON 的 byte 数组。原因开发者没能正确处理微信接口的两种返回值。wxacode.getUnlimited正常返回image/jpeg二进制流但参数错误时返回的是 JSON 文本两种情况的 HTTP 状态码都是 200。如果直接用字符串方法读取图片内容会被拉伸成乱码文本。解决响应处理统一走字节流路径。拿到Response后先读Content-Type包含image就按图片处理包含json就按错误处理更稳的方式是读字节数组后检查文件头是否为FF D8 FF是 JPEG 就继续。我习惯在工具类里封装一个checkImageMagic(byte[])方法所有实现方式共用一段校验逻辑。5.3 access_token 生命周期踩坑多实例互相踢下线现象接口随机返回{errcode:42001,errmsg:access_token expired}有时同一个请求上午成功、下午失败。原因服务用多实例部署每个实例各自维护一份 token 缓存实例 A 刷新了 token实例 B 还拿着旧 token 去调接口微信服务器发现 token 不再有效就返回 42001更隐蔽的一种情况是 A 和 B 同时刷新后者把前者的 token 作废。解决token 必须全局统一存储。多实例环境放 Rediskey 用wx:access_token:{appid}刷新时用SETNX加锁抢到锁的实例才允许调 token 接口其他实例等锁释放后从 Redis 再读刷新周期在 7200 秒上扣掉 200 秒余量。单实例部署也要把 token 从内存缓存改成文件或独立存储避免应用重启后突增一次 token 请求。5.4 小程序码可扫性踩坑生成了但扫不出来现象扫码后提示“页面不存在”或干脆识别失败但接口返回的明明是图片字节流。原因两种情况混在一起。一种是 page 指向的是未发布的体验版页面正式版小程序里没有这个路径扫码自然进不去另一种是开了auto_colortrue部分机型对浅色二维码的识别能力不足二维码前景和背景颜色对比度不够扫不出来。解决发布前在 app.json 确认页面路径真实存在并检查该页面是否在小程序后台配置了合法域名和业务域名二维码配色上不用auto_color手动指定line_color为深色比如#000000或定制品牌色中足够深的色值。还要提醒产品侧小程序码不能像普通二维码那样随意换底色视觉稿再好看识别率不行用户依旧扫不了。5.5 接口频控踩坑45009 接口调用超限现象接口返回{errcode:45009,errmsg:reach max api daily quota limit}。原因微信对wxacode.getUnlimited有每日调用上限具体额度与小程序认证状态和活跃度有关。常见误用是每次页面分享都临时调一次接口生成新码没有做缓存同一个渠道的二维码被反复请求日配额用完直接 403。解决二维码字节流在服务端做缓存key 用wx_code: scene page width 拼接的 MD5缓存时间按业务需求定我是默认缓存 24 小时失效后重新生成。渠道码这种长期不变的场景可以直接一次性生成后存入数据库按场景值读取。6. 进阶技巧字节流校验与二维码可扫性的验证闭环6.1 快速识别返回内容的真身无论用哪种 HTTP 实现请求微信接口拿到结果后第一件事是判断返回类型。把这段代码做成工具类所有实现方式共用public static byte[] validateWxCodeResponse(byte[] data) { if (data null || data.length 3) { throw new IllegalStateException(响应体为空); } // JPEG 文件头 FFD8FFPNG 文件头 89504E47 if ((data[0] 0xFF) 0xFF (data[1] 0xFF) 0xD8 (data[2] 0xFF) 0xFF) { return data; } // 不是 JPEG说明返回的是错误 JSON String errorText new String(data, StandardCharsets.UTF_8); throw new IllegalStateException(微信返回错误: errorText); }这方法不依赖任何 HTTP 对象纯粹从字节层面判断放在 Common 模块里五个实现方式都能复用。每次生成后强制走一遍能拦截大部分参数错误。6.2 二维码写文件与 scene 回读验证生成了码还要验证码里确实带了正确的 scene。把字节流写成 PNG 文件再用 ZXing 反向解析二维码内容File output new File(/tmp/wxcode.png); try (FileOutputStream fos new FileOutputStream(output)) { fos.write(qrCodeBytes); } // 用 ZXing 解析验证 BufferedImage image ImageIO.read(output); LuminanceSource source new BufferedImageLuminanceSource(image); BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); Result result new MultiFormatReader().decode(bitmap); System.out.println(result.getText()); // 输出 channelinviter_10086注意一个小程序码的编码内容是微信私有格式ZXing 不一定能 100% 解出全部字段但能解出大部分链接信息解不出来的情况也不要慌直接拿手机微信扫一下能进页面说明 scene 已经透传。验证完再落库别让错误的码进生产环境。6.3 我的验收习惯这个项目做完以后我已经形成一套固定的验收流程拿到字节流先用validateWxCodeResponse看文件头确认是 JPEG 不是 JSON再写本地文件用 ImageIO 打开能渲染成图片才继续最后用微信扫一次确认进入的页面和带上的参数都正确。走完这三步才把字节流传给前端或存库。这套流程看起来土但确实帮我拦下了不少问题——有一回场景里把渠道号写错了一位就是靠扫出来核对才发现省了线上投诉的麻烦。希望这份踩坑经验也能帮你少走一段弯路。本文还有配套的精品资源点击获取