通常来说,移动端的登录身份状态是通过token实现的,有关个人信息的资源和操作的服务端接口会要求传入token参数来实现身份验证。所以在大部分接口里我们都要写一个@Query("token")token: String,或者在构建Body的JsonBean里加一个token的属性。那能不能在retrofit的基础上进行扩展简单的完成这件事呢。
需求:通过自定义注解实现请求参数的注入
这里涉及两个问题,自定义注解的解析,请求参数的注入。
我们都知道retrofit是通过动态代理实现的,那么第一个问题就好解决了,可以通过再一次的动态代理也就是双重代理拿到方法注解的信息。
请求参数的问题,我们可以通过Okhttp的拦截器来实现。GET的方式的请求添加参数到url结尾,POST及其他方式需要重构RequestBody。
自定义注解:
@SIGN 方法注解,用于注解retrofit所代理的apiService的接口方法上
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface SIGN {
boolean value() default true;
}
双重代理:
不多说直接上代码:
T apiService = retrofit.create(tClass);
T newApiService = (T) newProxyInstance(tClass.getClassLoader(), new Class<?>[]{tClass}, new ProxyHandler(apiService));
上面代码很简单,tClass是被代理的接口,先调用retrofit的动态代理生成代理类,再调用一次动态代理,动态代理的逻辑实现在ProxyHandler。
ProxyHandler是一个实现InvocationHandler的类,我们需要在实现方法invoke里解析我们自定义的注解@SIGN 。
下面是invoke方法的核心代码:
SIGN signAnnotation = method.getAnnotation(SIGN.class);
boolean isSign = signAnnotation !=null && signAnnotation.value();
if(!isSign){return;}
String url = null;
for(Annotation annotation: method.getDeclaredAnnotations()){
if(annotation instanceof GET){
url = ((GET) annotation).value();
}else if(annotation instanceof POST){
url = ((POST) annotation).value();
}else if(annotation instanceof PUT){
url = ((PUT) annotation).value();
}else if(annotation instanceof DELETE){
url = ((DELETE) annotation).value();
}
}
//解析@Url
if(TextUtils.isEmpty(url)){
Annotation[][] annotations = method.getParameterAnnotations();
for(int i=0; i<annotations.length;i++){
for(int j=0; j<annotations[i].length;j++){
if(annotations[i][j] instanceof Url){
try {
String path = new URL((String)args[i]).getPath();
url = path.substring(1);
} catch (MalformedURLException e) {
e.printStackTrace();
}
break;
}
}
if(!TextUtils.isEmpty(url)) break;
}
}
if(!TextUtils.isEmpty(url)){
Timber.d(url);
SignUrlSet.INSTANCE.add(url);
}
我们先解析了方法是否被@SIGN标记,若存在则解析该接口的Url路径。(这里我没有使用全路径,需要全路径的请传入baseUrl)
在解析Url的时候我们并没有使用反射去通过retrofit来获取,感兴趣的同学可以尝试一下,这里不做赘述。
因为联网请求的异步特性存urlPath集合我使用了CopyOnWriteArraySet,避免出现线程安全问题:
public enum SignUrlSet {
INSTANCE;
private Set<String> signUrls = new CopyOnWriteArraySet<>();
public void add(String url){
signUrls.add(url);
}
public boolean contains(String url){
return signUrls.contains(url);
}
}
Okhttp拦截器:
先看代码(这个类kotlin代码):
object SignInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val path = request.url().encodedPath().removePrefix("/")
if (SignUrlSet.INSTANCE.contains(path)) {
if ("GET".equals(request.method())) {
val httpUrl = request.url().newBuilder()
.addQueryParameter("token", UserRepository.mToken)
.build()
request = request.newBuilder()
.url(httpUrl).build()
} else {
val requestBody = request.body()
if (requestBody != null) {
val buffer = Buffer()
requestBody.writeTo(buffer)
val charset = requestBody.contentType()?.charset() ?: Charset.forName("UTF-8")
val bodyContent = buffer.readString(charset)
val newBodyContent = Gson().fromJson(bodyContent, JsonObject::class.java)
.apply { addProperty("token", UserRepository.mToken) }
.toString()
request = request.newBuilder()
.method(request.method(), RequestBody.create(requestBody.contentType(), newBodyContent))
.build()
Timber.d(newBodyContent)
}
}
}
return chain.proceed(request)
}
}
我们在拦截器里拿到request,先通过从request解析出path路径,再去SignUrlSet查找是否存在该urlPath,也就是该请求方法是否被@SIGN标记。
再通过处理GET方式的Url和其他方式的RequestBody,来build一个新的请求。
自此我们就完成了只通过一个注解就可以实现token参数的注入:
GET方式:
@SIGN
@GET("base/u/user")
fun getPersonalInfo(): Observable>
POST以及其他方式:
data class ModifyLangReq(var language: String)
@SIGN
@PUT("base/u/language")
fun changeLanguage(@Body reqBody: ModifyLangReq): Observable<BaseResponse<Any>>
以上我们就成功的给Retrofit扩展了注入token的功能,其实通过这个方式还可以添加很多你需要的功能,比如很多业务需要用到的Token自动刷新、Token过期退出登录、加密、缓存、API版本号、多BaseUrl,都可以这样实现,有机会再和大家分享。