☰
Java服务端生成微信小程序二维码:5种HTTP客户端实现与选型
2026/10/20 7:23:07 网站建设 项目流程

简介:针对微信小程序二维码裂变分享与渠道推广场景,该资源汇总了基于Java/HTTP调用微信服务端接口生成小程序二维码的五种实现方式,适合需为每个账号生成专属邀请码、并打通前后端调用链路的开发者。项目从安全角度设计为前端→后端API→微信API,重点采用官方getUnlimitedQRCode接口,生成的码永久有效、数量不限,可支撑邀请有奖、渠道分成等业务。压缩包共69个文件,以XML配置、Java源码、properties配置文件、class字节码及依赖jar为主,整体仅53KB;其中50个XML用于Maven及IDE配置,4个Java承载核心调用逻辑,3个properties保存关键参数,5个class为编译产物。Maven工程目录清晰,可直接导入运行调试。目前已由2648人学习下载,配合完整源码可比较五种方案的适用边界,快速选型并部署到自己的邀请奖励或渠道推广流程中。

1. 微信小程序二维码生成:为什么 HTTP 接口是服务端的第一选择

接到一个营销需求:用户分享活动页,系统要生成一张带邀请参数的二维码海报。刚开始在前端用 canvas 拼图,小程序码走官方组件,参数一多就崩,scene 拼接稍长直接不识别,线上翻车好几次。最后把生成逻辑挪到 Java 服务端,统一走微信官方 HTTP 接口,前端只负责展示图片,问题才彻底解决。

这个源码包的核心就是这个场景的完整落地:用 Java 通过 HTTP 调用微信官方接口生成小程序码,并给出 5 种不同的实现方式。从 JDK 自带的 HttpURLConnection 到 Spring 的 RestTemplate、WebClient,同一套业务逻辑换了五种 HTTP 客户端写法,适合正在做小程序分享、海报、扫码核销这类需求的开发者。新手可以挑一种照着抄,熟手可以直接对比不同客户端的细节差异。

2. 接口与选型:先读懂微信官方接口,再谈 5 种实现

2.1 官方接口解读:getUnlimited 的请求与响应模型

微信小程序码相关接口有 3 个,日常业务里最常用的是getUnlimited,也就是「获取不限制数量的小程序码」。它的调用地址是:

POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN

请求体是 JSON,核心参数如下:

{ "scene": "a=1&b=2", "page": "pages/activity/index", "check_path": true, "env_version": "release", "width": 430, "auto_color": false, "line_color": {"r": 0, "g": 0, "b": 0} }

参数含义:scene是场景值,最大 32 个可见字符,只能包含数字、大小写英文以及部分特殊字符,这是二维码携带业务参数的唯一入口;page是小程序页面路径,必须是已发布页面;env_version指定打开的是开发版、体验版还是正式版;width控制码的尺寸;line_color是码的颜色。

这里要特别说清楚响应模型。接口成功时返回的不是 JSON,而是图片的二进制流;只有失败时才返回 JSON 格式的错误信息,例如{"errcode": 40001, "errmsg": "invalid credential"}。所以服务端拿到响应后不能直接按 JSON 解析,先看 Content-Type 或前几个字节,否则容易把图片数据读出来再去转字符串,白忙一场。这块逻辑就是 5 种实现里最需要小心的公共部分。

2.2 五种 HTTP 客户端的技术定位与选型对照

之所以要提供 5 种实现,不是因为「写法炫技」,而是不同团队的技术栈差异很大。老项目里还在用 JDK 自带的 HttpURLConnection,新项目可能已经全面切了 OkHttp 或者 WebClient。同一个服务里,不同模块也可能因为历史原因混用了多种 HTTP 组件。把这 5 种方式放在一起,方便按自己的项目现状对号入座。

实现方式依赖重量典型场景学习成本
HttpURLConnection无,JDK 内置零依赖小工具、无法引入第三方包中
Apache HttpClient 4.x中,需引入 httpclient传统 SSM 项目、老系统改造中
OkHttp轻,需引入 okhttpAndroid 与 Java 后端通用、追求性能低
RestTemplate中,Spring Web 自带Spring MVC / Spring Boot 传统项目低
WebClient重,需引入 webfluxSpring WebFlux / 响应式链路较高

选型上没有绝对最优。如果项目里已经用了 Spring Boot,RestTemplate 是成本最低的;如果服务里其他地方都在用 OkHttp,没必要为了一个二维码接口引入第二套 HTTP 客户端。这个包的 5 种实现都封装成了同一个方法签名,切换成本几乎为零,这也是服务端代码该有的样子——业务逻辑和 HTTP 细节解耦。

3. 核心代码拆解:5 种方式生成小程序码的完整实现

3.1 公共层:AccessToken 缓存与接口常量封装

在写 5 种 HTTP 客户端之前,先把公共部分抽出来。生成小程序码必须带access_token,这个 token 有效期 7200 秒,每次请求都去微信拉取一次显然不合理,常规做法是内存缓存加过期判断。这里定义一个WxApiClient作为门面,先看 token 管理:

public class WxAccessTokenManager { private final String appId; private final String appSecret; private volatile String accessToken; private volatile long expireAt; public WxAccessTokenManager(String appId, String appSecret) { this.appId = appId; this.appSecret = appSecret; } public synchronized String getToken() throws IOException { // 提前 5 分钟过期,避免 token 刚用完就失效 if (accessToken != null && System.currentTimeMillis() < expireAt - 300_000) { return accessToken; } String url = String.format( "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret); // 这里简化写逻辑,用任意 HTTP 客户端 GET 后解析 JSON 中的 access_token String json = httpGet(url); this.accessToken = parseToken(json); this.expireAt = System.currentTimeMillis() + 7200_000; return this.accessToken; } }

这块设计有个关键点:volatile保证多线程可见性,synchronized避免多个线程同时过期同时去刷新 token。过期时间提前 5 分钟是血泪经验,微信端网络抖动可能让 token 在临界点失效,提前刷新能少踩很多坑。

这里httpGet方法在 5 种实现里分别对应不通的 HTTP 客户端,门面类里再把「拿 token」和「生成小程序码」组合起来:

public byte[] generateWxCode(String scene, String page) throws IOException { String token = tokenManager.getToken(); // 5 种实现都实现这个接口,业务层不关心底层 HTTP 客户端 WxCodeGenerator generator = WxCodeGeneratorFactory.getGenerator(); return generator.fetchWxCode(token, scene, page); }

WxCodeGenerator是统一接口,5 种实现类都实现它。这样业务代码永远只依赖门面和接口,换 HTTP 客户端只改工厂类里的一个返回值。

3.2 方式一:原生 HttpURLConnection 实现

先看最朴素的写法。JDK 自带的 HttpURLConnection 不需要引入任何依赖,适合工具类场景。完整实现如下:

public class HttpUrlConnectionGenerator implements WxCodeGenerator { @Override public byte[] fetchWxCode(String accessToken, String scene, String page) throws IOException { URL url = new URL( "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setConnectTimeout(5000); conn.setReadTimeout(8000); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); String body = buildBody(scene, page); try (OutputStream os = conn.getOutputStream()) { os.write(body.getBytes(StandardCharsets.UTF_8)); } int status = conn.getResponseCode(); if (status == 200) { try (InputStream is = conn.getInputStream()) { return readAllBytes(is); } } else { // 非 200 时错误信息在 errorStream,别读错流 try (InputStream es = conn.getErrorStream()) { throw new IOException("http status: " + status + ", body: " + readAllBytes(es)); } } finally { conn.disconnect(); } } }

几个容易踩的坑都在注释里标了。setDoOutput(true)不加,POST 请求体根本发不出去;getErrorStream和getInputStream不能混读,状态码非 2xx 时微信返回的错误信息在 errorStream 里。连接池方面,HttpURLConnection 底层有 Keep-Alive,但这里每次都disconnect()更稳妥,避免连接悬挂。

3.3 方式二:Apache HttpClient 实现

老 SSM 项目里最常见的是 Apache HttpClient 4.x,很多框架都把它作为基础依赖带进来了。实现上有两个特点:连接池管理完善,响应处理方式灵活。

public class ApacheHttpClientGenerator implements WxCodeGenerator { private final CloseableHttpClient httpClient; public ApacheHttpClientGenerator() { // 连接池配置,生产环境按需调大 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(50); cm.setDefaultMaxPerRoute(20); httpClient = HttpClients.custom() .setConnectionManager(cm) .setConnectionTimeToLive(30, TimeUnit.SECONDS) .build(); } @Override public byte[] fetchWxCode(String accessToken, String scene, String page) throws IOException { HttpPost post = new HttpPost( "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken); post.setHeader("Content-Type", "application/json; charset=utf-8"); post.setEntity(new StringEntity(buildBody(scene, page), StandardCharsets.UTF_8)); try (CloseableHttpResponse response = httpClient.execute(post)) { int status = response.getStatusLine().getStatusCode(); byte[] body = EntityUtils.toByteArray(response.getEntity()); if (status != 200) { throw new IOException("weixin api error, status=" + status + ", body=" + new String(body, StandardCharsets.UTF_8)); } return body; } } }

Apache HttpClient 的StringEntity需要显式指定编码,否则中文场景值会乱码。EntityUtils.toByteArray会把整个响应读进内存,对小程序码这种几十 KB 的图片没问题,但如果哪天换成下载大文件,这个姿势就不合适了。连接池的setMaxTotal(50)是根据单个服务实例的并发量保守设置的,压测后再调也不迟。

3.4 方式三:OkHttp 实现

OkHttp 在 Android 和 Java 后端都很流行,API 设计比 Apache HttpClient 清爽不少。同一个功能,OkHttp 的代码量更少,语义也更直白。

public class OkHttpGenerator implements WxCodeGenerator { private final OkHttpClient client; public OkHttpGenerator() { client = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(8, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES)) .build(); } @Override public byte[] fetchWxCode(String accessToken, String scene, String page) throws IOException { RequestBody requestBody = RequestBody.create( buildBody(scene, page), MediaType.parse("application/json; charset=utf-8") ); Request request = new Request.Builder() .url("https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken) .post(requestBody) .build(); try (Response response = client.newCall(request).execute()) { byte[] body = response.body().bytes(); if (!response.isSuccessful()) { throw new IOException("weixin api error, code=" + response.code() + ", body=" + new String(body, StandardCharsets.UTF_8)); } return body; } } }

OkHttp 的try (Response ...)会自动释放连接,不需要手动关闭流。这里的response.body().bytes()会把整个响应体读进内存,OkHttp 官方不建议对大响应这么做,但小程序码图片通常只有几十 KB,完全够用。连接池参数里keepAlive 5 分钟是为了让短连接能被复用,频繁创建连接在小程序码这种高 QPS 场景下是浪费。

3.5 方式四:Spring RestTemplate 实现

Spring Boot 项目里 RestTemplate 最省事,但有个细节:Spring Boot 3.x 之后不再自动装配 RestTemplate,需要手动创建 Bean。实现代码如下:

public class RestTemplateGenerator implements WxCodeGenerator { private final RestTemplate restTemplate; public RestTemplateGenerator() { // 用 SimpleClientHttpRequestFactory,底层还是 HttpURLConnection SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(8000); restTemplate = new RestTemplate(factory); } @Override public byte[] fetchWxCode(String accessToken, String scene, String page) throws IOException { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<String> entity = new HttpEntity<>(buildBody(scene, page), headers); String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken; // 关键:不指定返回类型,按字节数组接收,保留原始响应 ResponseEntity<byte[]> response = restTemplate.exchange( url, HttpMethod.POST, entity, byte[].class); if (!response.getStatusCode().is2xxSuccessful()) { throw new IOException("weixin api error, status=" + response.getStatusCode()); } return response.getBody(); } }

RestTemplate 最容易翻车的是用String.class接响应。微信返回图片二进制流,如果指定 String,框架会按默认字符集解码,图片数据被转码后大概率损坏。这里必须用byte[].class接。另外,RestTemplate 默认的SimpleClientHttpRequestFactory底层也是 HttpURLConnection,想上连接池得换成HttpComponentsClientHttpRequestFactory,底层再包一层 Apache HttpClient。

3.6 方式五:Spring WebClient 实现

WebClient 是 Spring WebFlux 的响应式 HTTP 客户端。用它的场景通常是项目里已经在跑响应式链路,或者想统一用最新的 Spring 风格。代码是异步的,但用.block()强行转同步也不会出问题:

public class WebClientGenerator implements WxCodeGenerator { private final WebClient webClient; public WebClientGenerator() { webClient = WebClient.builder() .baseUrl("https://api.weixin.qq.com") .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8") .build(); } @Override public byte[] fetchWxCode(String accessToken, String scene, String page) { return webClient.post() .uri("/wxa/getwxacodeunlimit?access_token={token}", accessToken) .bodyValue(buildBody(scene, page)) .retrieve() .bodyToMono(byte[].class) .block(); } }

bodyToMono(byte[].class)对应二进制响应。WebClient 在响应式项目里是标配,但如果是传统的 Servlet 项目引入 WebClient,还要额外引入spring-boot-starter-webflux依赖,包体积和启动成本都要考虑。.block()会阻塞当前线程,在响应式链路里这是个需要谨慎的做法,通常用来兼容旧代码或测试环境。

3.7 输出层:二维码图片保存与响应回写

无论哪种方式,拿到的是byte[],后续处理分两条路:保存到本地/OSS,或者直接回写给前端。保存到本地很直接:

public void saveToFile(byte[] data, String fileName) throws IOException { Path path = Paths.get("/data/wxcode", fileName + ".png"); Files.write(path, data); }

回写给前端时要注意响应头。小程序码图片是 PNG 格式,Content-Type 必须是image/png,还要设置Content-Length,否则前端可能拿到分块传输的流,某些 Android WebView 会解析失败。实际项目里我一般这样返回:

@GetMapping("/wxcode") public ResponseEntity<byte[]> wxcode(@RequestParam String scene, @RequestParam String page) throws IOException { byte[] image = wxApiClient.generateWxCode(scene, page); return ResponseEntity.ok() .contentType(MediaType.IMAGE_PNG) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=wxcode.png") .body(image); }

Content-Disposition设为inline表示浏览器直接展示而不是触发下载,这一步容易漏。到这里,5 种实现已经能独立完成「拿 token → 调接口 → 处理图片」的完整链路了。

4. 跑起来:工程结构、参数配置与本地验证

4.1 工程目录与依赖清单

源码包是标准的 Maven 工程,结构如下:

wx-code-generator ├── pom.xml ├── src/main/java/com/example/wxcode │ ├── WxApiClient.java // 门面类 │ ├── WxAccessTokenManager.java // token 管理 │ ├── generator/ │ │ ├── WxCodeGenerator.java // 统一接口 │ │ ├── WxCodeGeneratorFactory.java // 工厂类,切换实现 │ │ ├── HttpUrlConnectionGenerator.java │ │ ├── ApacheHttpClientGenerator.java │ │ ├── OkHttpGenerator.java │ │ ├── RestTemplateGenerator.java │ │ └── WebClientGenerator.java │ └── controller/WxCodeController.java // 对外 HTTP 接口 └── src/main/resources/application.yml

pom.xml里按需引入依赖。如果只验证其中一种实现,可以注释掉多余的依赖,但注意 RestTemplate 和 WebClient 两种实现分别属于spring-web与spring-webflux,同时引入两个模块时 Spring Boot 会自动配置,一般不会冲突,启动时会多打印一些自动配置日志。

4.2 配置文件与测试入口

application.yml里只需要两个微信参数:

wx: app-id: 你自己的appid app-secret: 你自己的secret

这两个值在小程序后台的「开发管理 → 开发设置」里拿。注意小程序码接口只能由小程序账号调用,普通公众号的 appid 调不通。

写一个简单的测试入口,不启动 Web 容器也能验证:

@SpringBootTest class WxCodeGeneratorApplicationTests { @Autowired private WxApiClient wxApiClient; @Test void testGenerate() throws IOException { byte[] bytes = wxApiClient.generateWxCode("invite=1001", "pages/index/index"); System.out.println("生成成功,大小 = " + bytes.length + " bytes"); // 本地保存看效果 Files.write(Paths.get("/tmp/wxcode.png"), bytes); } }

这里scene=invite=1001是「键值对直接拼」的写法,微信要求 scene 是固定格式字符串,可以自己约定a=1&b=2这种明文,也可以直接用纯字符串1001,由后端自行解析。

4.3 从启动到拿到二维码的验证路径

一次完整验证步骤是这样:

  1. 修改application.yml里的 appid 和 secret。
  2. 运行测试方法,观察日志里是否打印「生成成功」。
  3. 打开/tmp/wxcode.png,扫码跳转到对应小程序页面。
  4. 再次运行测试,确认第二次没有重新请求 token 接口(token 缓存生效)。

如果不想跑测试类,也可以直接启动应用,访问http://localhost:8080/wxcode?scene=invite=1001&page=pages/index/index,浏览器会直接渲染出二维码图片。这一步能通,说明 5 种实现都能正常工作。

工厂类的切换逻辑如下:

public class WxCodeGeneratorFactory { public static WxCodeGenerator getGenerator() { // 想用哪种就返回哪个实例,例如: // return new OkHttpGenerator(); return new RestTemplateGenerator(); } }

切换实现时只需要改这一行,方便对比 5 种方式的效果。一般验证顺序是从 HttpURLConnection 开始,逐步换成 OkHttp、RestTemplate,感受一下代码量的变化。

5. 避坑盘点:token 过期、流关闭和返回码误判的 5 个血泪记录

5.1 接口调用层面的坑

现象一:调用接口返回errcode 40001,但 token 明明刚获取。

原因是 access_token 在多个服务实例间没有共享,每个实例各拿各的,微信对同一 appid 的 token 获取频率有限制,后获取的 token 会使先获取的 token 失效。解决方式:把 access_token 放到 Redis,按过期时间减 5 分钟存储;多个实例都从 Redis 读。单机调试时一般不触发,一上多实例就暴露。

现象二:请求成功但二维码图片扫描后打不开小程序。

原因是page路径写错了,或者页面没有发布到对应环境。getUnlimited的 page 必须是已发布页,如果传了env_version=trial且页面只存在于开发版,扫码就会提示页面不存在。解决方式:先在小程序开发者工具里确认页面路径,再对照env_version的值。

现象三:scene 里带中文或&符号,部分二维码识别失败。

原因是scene参数对字符集有限制,只支持数字、大小写英文和!#$&'()*+,/:;=?@-._~这些字符,中文必须 URL 编码,但编码后的%本身也在受限范围里。解决方式:scene 里只放业务主键 ID,其他参数在服务端根据 ID 反查,别把完整参数塞进去。

5.2 工程落地层面的坑

现象四:压测时大量连接超时,Tomcat 线程被挂起。

原因是 HttpClient 或 OkHttp 的全局实例被反复创建,每个实例都有自己的连接池,连接池被耗尽后新建连接需要时间。解决方式:把 5 种客户端实例都做成单例,Spring 容器里用@Component管理,不要每次请求都new。

现象五:图片字节流被当字符串解析,输出到前端后图片花屏。

原因是响应接收类型用了String.class或InputStreamReader,二进制被按字符集解码再编码,数据损坏。解决方式:统一用byte[]接收,回写时设置MediaType.IMAGE_PNG。这个坑在 RestTemplate 和 Apache HttpClient 两种写法里最容易出现,我一开始在 RestTemplate 里用String.class接,调试了一个下午才发现是这里的问题。

6. 二次开发改造:把生成服务改造成带缓存与重试的通用模块

一个很实际的改造需求:同一个 scene 在小程序码的有效期内多次生成,微信接口每次都返回新图片,对调用方来说既不必要也占用配额。我一般会在这个服务里加一层本地缓存,以 scene 为 key,把图片字节和时间戳一起缓存。改造后的门面方法长这样:

private final Map<String, CacheItem> cacheMap = new ConcurrentHashMap<>(); public byte[] generateWxCodeWithCache(String scene, String page) throws IOException { String cacheKey = scene + "|" + page; CacheItem cached = cacheMap.get(cacheKey); if (cached != null && cached.expireAt > System.currentTimeMillis()) { return cached.data; } byte[] data = generateWxCode(scene, page); cacheMap.put(cacheKey, new CacheItem(data, System.currentTimeMillis() + 3600_000)); return data; }

缓存时间我通常设 1 小时,如果业务上希望用户每次分享都能被追踪,这个时间可以缩短。真正的生产环境可以把Map换成 Caffeine,按容量淘汰,避免内存无限增长。

另一个值得加的是重试机制。微信接口偶尔会抖动,返回 500 或超时,直接在fetchWxCode外层包一层重试即可。重试只针对「网络异常」和「部分可重试错误码」,40001 token 失效这类错误重试也没用,要先刷新 token。从那以后我每次写外部接口调用都会强制走一遍「缓存 + 超时 + 重试」的检查清单,别让生成二维码这种小功能在线上变成黑匣子。希望帮到你,跑通之后改成带缓存和重试的版本,这一步值得做。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询