背景:
对消息发送平台的客户端,封装一个sdk包,以供其他项目使用
选择Retrofit框架的优点
使用Retrofit框架封装httpclient客户端,优点是Retrofit将远程http调用,在代码上封装成了类似微服务的接口定义调用方式,代码上较为简洁,使用的工具包也比较轻量。
使用方式示例:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
// 采用谷歌的gson序列化工厂
.addConverterFactory(GsonConverterFactory.create())
.build();
ServiceName serviceName = retrofit.create(ServiceName.class);
// 返回接口声明的参数
Response<Rest<SendSmsResponse>> response = serviceName.methodName().execute();
Rest<SendSmsResponse> rep = response.body();
由于这样使用的不同的service均需要在使用时create,所以在设计时定义一个共有子接口ActionService(为什么不定义为父级口,因为父级口中是无方法的,无法在使用时调用),把ActionService作为自定义client的一个属性,一次创建即可使用。
自定义client的设计:
首先是对自定义client的创建,可以参考aliyunOSS的客户端,设计的需求是在任一系统中一次性创建client,并且项目开发者无需关注远程调用时出现的异常,发送短信/语音消息时,类似工具类的静态方法形式使用。
- client使用单例模式,使用AtomicReference类存储client对象,build方法里面AtomicReference的compareAndSet方法,如果预期值是null则创建client,不为null则返回client对象,就完成了优化版无需加锁的单例模式
- ActionService作为接口的子接口,统一方法的输出,目的是在一个客户端中使用所有service且不用手动创建
- 将token的维护放在retrofit框架中的自定义拦截器中
/**
* @author: xiaoxiaochengxuyuan
* @Description: 消息客户端
* @Date: 2022/08/30/4:36 下午
*/
@Data
@SuppressWarnings("unchecked")
public class MessageClient {
private static final AtomicReference<MessageClient> INSTANCE = new AtomicReference<>();
private String clientId;
private String clientSecret;
private String baseUrl;
private Retrofit retrofit;
/**
* 方法封装service
*/
private ActionService actionService;
private MessageClient(String clientId, String clientSecret, String baseUrl) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = baseUrl;
// 构建OkHttpClient添加自定义拦截器 超时时间、链接时间默认为10秒
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor(clientId, clientSecret, baseUrl));
;
// 构建Retrofit客户端
this.retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(builder.build())
.build();
// 注入所有方法
this.actionService = retrofit.create(ActionService.class);
}
public static MessageClient build(String clientId, String clientSecret, String baseUrl) {
while (true) {
MessageClient messageClient = INSTANCE.get();
if (null != messageClient) {
return INSTANCE.get();
}
if (INSTANCE.compareAndSet(null, new MessageClient(clientId, clientSecret, baseUrl))) {
return INSTANCE.get();
}
}
}
/**
* 发送短信消息
*
* @param request
* @throws IOException
*/
public Rest<SendSmsResponse> sendSms(SendSmsRequest request) throws IOException {
return null;
}
/**
* 语音消息详情
*
* @param request
* @throws IOException
*/
public Rest<VmsDetailResponse> callDetail(VmsDetailRequest request) throws IOException {
return null;
}
/**
* 统一异常响应对象
*
* @param result
* @return
*/
private Rest<? extends BaseResponse> getErrorBody(Response<? extends Rest<? extends BaseResponse>> result) {
String message = "";
try (ResponseBody errorBody = result.errorBody()) {
if (errorBody != null) {
String msg = errorBody.source().readUtf8();
if (StrUtil.isNotBlank(msg)) {
message = msg;
}
}
} catch (IOException | JsonSyntaxException e) {
message = "未知的错误";
}
return new GsonBuilder()
.setExclusionStrategies(new GsonExcludeStrategy())
.serializeNulls()
.create()
.fromJson(message, Rest.class);
}
}
retrofit的service方法声明如下,熟悉Java的应该看一眼就明白的差不多了,出参入参如常见的controller层一样,方法的类型Get还是Post都使用注解加地址的形式声明,参数可以是json对象(@Body注解修饰,效果同@RequestBody),而方法的声明如微服务一样,接口声明无需方法体。
在使用时通过retrofit.create(ServiceName.class).methodName().execute();
/**
* @author: xiaoxiaochengxuyuan
* @Description: 汇总方法接口,如有新添加的service需要添加 extends
* @Date: 2022/08/31/3:11 下午
*/
public interface ActionService extends OperationService {
}
/**
* @author: xiaoxiaochengxuyuan
* @Description: 消息平台controller层
* @Date: 2022/08/31/2:48 下午
*/
public interface OperationService {
/**
* 短信发送
*
* @param request
* @return
*/
@POST("/openapi/sms/send")
Call<Rest<SendSmsResponse>> sendSms(@Body SendSmsRequest request);
/**
* 语音提醒
*
* @param request
* @return
*/
@POST("/openapi/vms/send")
Call<Rest<SendSmsResponse>> sendVms(@Body SendVmsRequest request);
}
public interface AuthService {
/**
* 获取消息平台授权
*
* @return
*/
@POST("/oauth2/token")
@FormUrlEncoded
Call<AuthorizationResponse> authorization(@Field("client_id") String clientId, @Field("client_secret") String clientSecret, @Field("grant_type") String grant_type);
}
拦截器的使用
Interceptor接口是retrofit框架中自定义的拦截器接口,不是spring中的拦截器。在方法 intercept(Chain chain)中手动校验token的有效性,然后重新构建Request传入有效的token参数(为什么重新构建Request,retrofit暴露的修改请求参数的方法只能是新建),AuthorizationResponse中封装了过期时间、token的值等参数。
其实这里可以将token的监控放在client类中,优点是retrofit只创建一次即可,代码复杂度会低一点。缺点是clinet中的功能不单一,与单一功能原则违背。有心思的兄弟们可以自己研究下更优的方式。
/**
* @author: xiaoxiaochengxuyuan
* @Description: header添加参数拦截器
* @Date: 2022/08/30/6:35 下午
*/
public class HeaderInterceptor implements Interceptor {
private final static String TOKEN_HEADER = "Authorization";
private final String clientId;
private final String clientSecret;
private final AuthService authService;
private AuthorizationResponse tokenHolder;
public HeaderInterceptor(String clientId, String clientSecret, String baseUrl) {
this.clientId = clientId;
this.clientSecret = clientSecret;
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build();
// 注入authService方法
this.authService = retrofit.create(AuthService.class);
}
@Override
public Response intercept(Chain chain) throws IOException {
if (tokenHolder == null
|| LocalDateTime.now()
.isAfter(LocalDateTime.ofInstant(Instant.ofEpochMilli(tokenHolder.getExpiresIn())
, ZoneId.systemDefault()))) {
tokenHolder = authorization();
}
Request newRequest = chain
.request()
.newBuilder()
.addHeader(TOKEN_HEADER, tokenHolder.getTokenType() + " " + tokenHolder.getAccessToken())
.build();
return chain.proceed(newRequest);
}
/**
* 请求客户端授权
*
* @throws IOException
*/
private AuthorizationResponse authorization() throws IOException {
// 获取授权请求
Call<AuthorizationResponse> authorizationCall = this.authService.authorization(clientId, clientSecret, "client_credentials");
retrofit2.Response<AuthorizationResponse> executeResult = authorizationCall.execute();
for (int i = 0; i < 3; i++) {
if (!executeResult.isSuccessful()) {
// 授权重试机制
executeResult = this.authService.authorization(clientId, clientSecret, "client_credentials").execute();
} else {
break;
}
}
if (!executeResult.isSuccessful() || null == executeResult.body()) {
throw new ServiceException(MessageErrorCode.ACCESS_DENIED, null, "获取客户端授权失败", "获取客户端授权失败");
}
AuthorizationResponse authorizationResponse = executeResult.body();
authorizationResponse.setExpiresIn(LocalDateTime.now().plusSeconds((long) (authorizationResponse.getExpiresIn() * 0.8)).toInstant(ZoneOffset.of("+8")).toEpochMilli());
return authorizationResponse;
}
简单展示下sdk的使用方式:
// 在业务项目中构建client工具类
public class SmsClient {
private static MessageClient client;
static {
// 这里是简单展示通过static代码块创建
// 实际使用可以在代码中可以创建一个properties静态常量类把参数放到配置文件中
// 然后声明一个bean对象调用build方法注入properties中的密钥和host参数
// 交给spring管理后,这里使用@Autowired注入使用
String accessKeyId = "accessKeyId";
String accessKeySecret = "accessKeySecret";
String host = "https://host.com/";
client = MessageClient.build(accessKeyId, accessKeySecret, host);
}
public static void sendSms(SendSmsRequest request) {
try {
Rest<SendSmsResponse> sendSmsResponseRest = client.sendSms(request);
if (!sendSmsResponseRest.getSuccess()) {
log.error(StrUtil.format("短信发送失败code:{}, msg:{}", sendSmsResponseRest.getCode(), sendSmsResponseRest.getMsg()));
}
} catch (Exception e) {
log.error(StrUtil.format("短信发送失败:{}", request));
e.printStackTrace();
}
}
}
// 业务代码中使用:
SmsClient.sendSms(request);