在使用RestTemplate请求三方接口时:三方接口一般都要求在url后面拼接上固定的几个参数,一般如accessToken
进行权限校验。而我们在开发时,请求这些地址,如何避免在url拼接accessToken
这种重复固定的编码操作呢。
方法当然有很多,本文提供一种通过反射偷梁换柱的写法来实现。
- 以微信小程序服务端接口请求作为请求对象。
- 微信小程序要求在请求时带上
?accesss_token=ACCESS_TOKEN
如何实现..?
# 基础配置
- 微信小程序配置类
/** * 微信小程序配置类 * * @author futao * @date 2020/10/29 */ @ConfigurationProperties(prefix = WxMiniProgramProperties.PROPERTY_PREFIX) public class WxMiniProgramProperties { /** * 微信小程序配置前缀 */ public static final String PROPERTY_PREFIX = Consts.System.FRAMEWORK_BASE_NAME + "." + Consts.WxMiniProgram.WX_MINI_PROGRAM_BASE_NAME; /** * AppID(小程序ID) */ private String appId; /** * AppSecret(小程序密钥) */ private String appSecret; public String getAppId() { if (StringUtils.isBlank(appId)) { throw new WxMiniProgramException("微信小程序AppId未设置"); } return appId; } public void setAppId(String appId) { this.appId = appId; } public String getAppSecret() { if (StringUtils.isBlank(appSecret)) { throw new WxMiniProgramException("微信小程序AppSecret未设置"); } return appSecret; } public void setAppSecret(String appSecret) { this.appSecret = appSecret; } }
- 获取微信小程序accessToken
/** * 微信小程序AccessToken * * @author futao * @date 2020/10/29 */ @Slf4j @Service public class AccessTokenServiceImpl implements AccessTokenService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private WxMiniProgramProperties wxMiniProgramProperties; /** * 获取token * * @return token */ @Override public String get() { String redisAccessToken = redisTemplate.opsForValue().get(RedisKeyConsts.WxMiniProgram.WX_ACCESS_TOKEN); if (StringUtils.isBlank(redisAccessToken)) { //无缓存 String url = UriComponentsBuilder .fromHttpUrl(Consts.WxMiniProgram.WX_API_DOMAIN + "/cgi-bin/token") .queryParam("grant_type", "client_credential") .queryParam("appid", wxMiniProgramProperties.getAppId()) .queryParam("secret", wxMiniProgramProperties.getAppSecret()) .build() .encode() .toString(); ResponseEntity<AccessToken> accessTokenResponseEntity = WxMiniProgramConfig.REST_TEMPLATE.getForEntity(url, AccessToken.class); AccessToken accessToken = accessTokenResponseEntity.getBody(); String token = accessToken.getAccessToken(); redisTemplate.opsForValue().set(RedisKeyConsts.WxMiniProgram.WX_ACCESS_TOKEN, token, accessToken.getExpiresIn() - 5, TimeUnit.SECONDS); return token; } else { // 缓存命中 log.info("cache hint"); return redisAccessToken; } } }
- 想要请求的接口: GET https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=ACCESS_TOKEN&unionid=UNIONID
一、 每个接口都手动拼上accessToken
/** * 动态消息 * * @author futao * @date 2020/10/30 */ @Service public class DynamicMessageServiceImpl implements DynamicMessageService { @Autowired private AccessTokenService accessTokenService; /** * 创建被分享动态消息或私密消息的 activity_id * * @return */ @Override public DynamicMessageCreateResult createActivityId() { String url = UriComponentsBuilder .fromHttpUrl(Consts.WxMiniProgram.WX_API_DOMAIN + "/cgi-bin/message/wxopen/activityid/create") // 手动加上请求参数accessToken .queryParam("access_token", accessTokenService.get()) .build() .encode() .toString(); ResponseEntity<DynamicMessageCreateResult> messageCreateResultResponseEntity = WxMiniProgramConfig.REST_TEMPLATE.getForEntity(url, DynamicMessageCreateResult.class); DynamicMessageCreateResult createResult = messageCreateResultResponseEntity.getBody(); return createResult; } }
- 测试
/** * @author futao * @date 2020/10/30 */ @RequestMapping("/wx/mini") @RestController public class WxMiniController { @Autowired private DynamicMessageService dynamicMessageService; @GetMapping("/createDynamicMessage") public DynamicMessageCreateResult createDynamicMessage() { return dynamicMessageService.createActivityId(); } }
- 测试结果
- 功能是实现了,但是非常繁琐。
编码时,1.在每个调用微信小程序接口的地方,都加上accessToken参数
,由于该参数又依赖于AccessTokenService
,所以又需要先注入AccessTokenService
,比较繁琐。且,2.如果固定的请求参数不止一个而有很多个
,3.且来源比较复杂
,将极大地增加开发的繁琐程度。且,4.如果后续参数有调整
,有增减,那散落在各处的请求地址,每个都需要改,想想都可怕😨。
- 综合以上四点问题,迫切需要统一处理这些请求参数。
二、 拦截RestTemplate请求地址,给请求地址添加参数并替换原有地址
- RestTemplate拦截器
/** * @author futao * @date 2020/10/29 */ @Slf4j @Configuration public class WxMiniProgramConfig { private static AccessTokenService ACCESS_TOKEN_SERVICE; /** * 忽略的Path的集合 */ private static final Set<String> IGNORE_PATH_SET = new HashSet<>(); @Autowired private AccessTokenService accessTokenService; /** * PostConstruct注解的方法将会在依赖注入完成后被自动调用 */ @PostConstruct public void setWxMiniProgramProperties() { WxMiniProgramConfig.ACCESS_TOKEN_SERVICE = accessTokenService; } /** * 增强过的RestTemplate */ public static final RestTemplate REST_TEMPLATE = new RestTemplate(); static { //兼容text/plain WxMiniProgramConfig.REST_TEMPLATE.getMessageConverters() .add(new TextPlainHttpMessageConverter()); //需要忽略的地址: 请求token IGNORE_PATH_SET.add("/cgi-bin/token"); // 添加拦截器 WxMiniProgramConfig.REST_TEMPLATE.getInterceptors().add(new ClientHttpRequestInterceptor() { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { URI uri = request.getURI(); // 原始请求参数 String rawQueryString = uri.getRawQuery(); if (!IGNORE_PATH_SET.contains(uri.getRawPath())) { String queryStringToAppend = "access_token=" + WxMiniProgramConfig.ACCESS_TOKEN_SERVICE.get(); //追加之后的请求参数 String qsAfterAppend = StringUtils.isBlank(rawQueryString) ? queryStringToAppend : rawQueryString + "&" + queryStringToAppend; try { Field stringField = URI.class.getDeclaredField("string"); stringField.setAccessible(true); // 完整请求路径 String completeUrl = uri.getScheme() + "://" + uri.getHost() + uri.getPath() + "?" + qsAfterAppend; // 重新设置完整请求路径 stringField.set(uri, completeUrl); log.debug("request complete url:{}", completeUrl); } catch (NoSuchFieldException | IllegalAccessException e) { log.error("反射异常", e); throw new WxMiniProgramException("反射异常", e); } } else { log.debug("ignore path :{}", uri.getPath()); } ClientHttpResponse httpResponse = execution.execute(request, body); if (!httpResponse.getStatusCode().is2xxSuccessful()) { throw new WxMiniProgramException("访问微信小程序服务器失败:" + httpResponse.getStatusText()); } return httpResponse; } }); } /** * 兼容text/plain */ static class TextPlainHttpMessageConverter extends MappingJackson2HttpMessageConverter { public TextPlainHttpMessageConverter() { ArrayList<MediaType> supportedMediaTypes = new ArrayList<>(1); supportedMediaTypes.add(MediaType.TEXT_PLAIN); this.setSupportedMediaTypes(supportedMediaTypes); } } }
- service不再需要手动拼接参数
替换字段string
的值,而不是字段query
,是因为debug后发现,最终请求的地址是string
这个字段的值。
测试
- 已自动加上了access_token
- 可以愉快地CRUD惹
三、 其他
- 将拦截器封装成通用的方法
/** * 追加请求参数queryString的拦截器 * * @param paramsToAppend 需要追加的参数 * @param ignorePathSet 忽略的path的集合 * @return 拦截器 */ public static ClientHttpRequestInterceptor appendUrlQueryStringInterceptor(Map<String, Object> paramsToAppend, Set<String> ignorePathSet) { return (httpRequest, bytes, clientHttpRequestExecution) -> { if (paramsToAppend != null && paramsToAppend.size() > 0) { URI uri = httpRequest.getURI(); // 未忽略 if (ignorePathSet == null || (!ignorePathSet.contains(uri.getPath()))) { //当前查询字符串 String rawQueryString = uri.getRawQuery(); StringBuffer sb = new StringBuffer(); paramsToAppend.forEach((k, v) -> sb.append(k) .append("=") .append(v) .append("&")); // 需要追加的queryString String queryStringToAppend = sb.toString(); if (queryStringToAppend.endsWith("&")) { queryStringToAppend = queryStringToAppend.substring(0, queryStringToAppend.lastIndexOf("&")); } //追加之后的请求参数 String qsAfterAppend = StringUtils.isBlank(rawQueryString) ? queryStringToAppend : rawQueryString + "&" + queryStringToAppend; try { Field stringField = URI.class.getDeclaredField("string"); stringField.setAccessible(true); // 完整请求路径 String completeUrl = uri.getScheme() + "://" + uri.getHost() + uri.getPath() + "?" + qsAfterAppend; stringField.set(uri, completeUrl); log.debug("request complete url:{}", completeUrl); } catch (NoSuchFieldException | IllegalAccessException e) { log.error("反射异常", e); throw new WxMiniProgramException("反射异常", e); } } } ClientHttpResponse httpResponse = clientHttpRequestExecution.execute(httpRequest, bytes); if (!httpResponse.getStatusCode().is2xxSuccessful()) { throw new WxMiniProgramException("访问微信小程序服务器失败:" + httpResponse.getStatusText()); } return httpResponse; }; }
- 使用
setDefaultUriVariables()