在使用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
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请求地址,给请求地址添加参数并替换原有地址
- 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
这个字段的值。
image.png
image.png
- 测试
- 已自动加上了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()
网友评论