在使用RestTemplate请求三方接口时:三方接口一般都要求在url后面拼接上固定的几个参数,一般如accessToken进行权限校验。而我们在开发时,请求这些地址,如何避免在url拼接accessToken这种重复固定的编码操作呢。
方法当然有很多,本文提供一种通过反射偷梁换柱的写法来实现。
微信小程序要求在请求时带上 ?accesss_token=ACCESS_TOKEN
image.png
如何实现..?
# 基础配置
/** * 微信小程序配置类 * * @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 * * @author futao * @date 2020/10/29 */ @Slf 4j@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
image.png
一、 每个接口都手动拼上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(); } }
image.png
编码时,1.在每个调用微信小程序接口的地方,都加上accessToken参数,由于该参数又依赖于AccessTokenService,所以又需要先注入AccessTokenService,比较繁琐。且,2.如果固定的请求参数不止一个而有很多个,3.且来源比较复杂,将极大地增加开发的繁琐程度。且,4.如果后续参数有调整,有增减,那散落在各处的请求地址,每个都需要改,想想都可怕😨。
二、 拦截RestTemplate请求地址,给请求地址添加参数并替换原有地址
/** * @author futao * @date 2020/10/29 */ @Slf 4j@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); } } }
image.png
替换字段 string的值,而不是字段 query,是因为debug后发现,最终请求的地址是 string这个字段的值。
image.png
三、 其他
/** * 追加请求参数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()
欢迎在评论区留下你看文章时的思考,及时说出,有助于加深记忆和理解,还能和像你一样也喜欢这个话题的读者相遇~