分享http客户端框架Retrofit的使用,包含优化单例模式的client创建和Retrofit框架自带的Interceptor定制化

背景:

对消息发送平台的客户端,封装一个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,并且项目开发者无需关注远程调用时出现的异常,发送短信/语音消息时,类似工具类的静态方法形式使用。

  1. client使用单例模式,使用AtomicReference类存储client对象,build方法里面AtomicReference的compareAndSet方法,如果预期值是null则创建client,不为null则返回client对象,就完成了优化版无需加锁的单例模式
  2. ActionService作为接口的子接口,统一方法的输出,目的是在一个客户端中使用所有service且不用手动创建
  3. 将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);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值