前言
在上一篇文章中,我们从整体架构的角度分析了 Atlas HTTP Client 框架的设计思路。从本篇开始,我们将深入到具体的实现细节。首先要实现的就是框架的核心 ------ 注解系统。
注解系统是声明式 API 的基础,它定义了用户如何描述 HTTP 接口。一个好的注解系统应该:
- 直观易懂:注解名称和参数一目了然
- 功能完整:覆盖 HTTP 协议的主要特性
- 扩展性强:便于后续功能扩展
- 类型安全:编译时就能发现错误
注解系统设计原则
1. 语义化命名
注解的命名应该直接反映其功能,让用户无需查看文档就能理解其作用:
java
@GET("/users") // 一眼就知道是 GET 请求
@Path("id") // 明显是路径参数
@Query("name") // 显然是查询参数
@Body // 请求体参数
2. 最小化配置
遵循"约定优于配置"原则,为常用场景提供合理的默认值:
java
@HttpClient("https://api.example.com") // 只需要指定基础 URL
public interface UserService {
@GET("/users") // 默认返回 JSON,自动序列化
List<User> getUsers();
}
3. 组合使用
不同注解可以组合使用,实现复杂的功能:
java
@POST("/users")
@Header("Content-Type: application/json")
@Async(executor = "customExecutor")
CompletableFuture<User> createUser(@Body User user, @Header("Authorization") String token);
核心注解详细设计
1. @HttpClient - 客户端标识注解
这是最重要的注解,用于标识一个接口是 HTTP 客户端:
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* HTTP客户端注解
* 用于标识一个接口是HTTP客户端,并配置全局属性
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpClient {
/**
* 基础URL,与baseUrl()等价
*/
String value() default "";
/**
* 基础URL
*/
String baseUrl() default "";
/**
* 连接超时时间(毫秒)
*/
int connectTimeout() default 5000;
/**
* 读取超时时间(毫秒)
*/
int readTimeout() default 10000;
/**
* 是否默认异步执行
*/
boolean async() default false;
/**
* 默认执行器名称(用于异步执行)
*/
String executor() default "";
}
设计要点:
- value() 和 baseUrl() 的设计:提供两种方式指定基础 URL,value() 是简化写法
- 超时配置:提供连接和读取两种超时配置,满足不同场景需求
- 异步支持:全局异步配置,简化异步接口的定义
- 执行器配置:支持自定义线程池
2. HTTP 方法注解
为每种 HTTP 方法定义对应的注解:
@GET 注解
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* GET请求注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GET {
/**
* 请求路径
*/
String value();
}
@POST 注解
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* POST请求注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface POST {
/**
* 请求路径
*/
String value();
}
类似地,我们还需要定义 @PUT 和 @DELETE 注解。
设计要点:
- 统一的设计模式:所有 HTTP 方法注解都有相同的结构
- 路径参数:value() 参数用于指定请求路径
- 方法级别:只能用于方法上,不能用于类或字段
3. 参数注解
参数注解用于标识方法参数在 HTTP 请求中的作用:
@Path - 路径参数
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 路径参数注解
* 用于替换URL中的占位符
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Path {
/**
* 参数名称,对应URL中的占位符
*/
String value();
}
使用示例:
java
@GET("/users/{id}")
User getUser(@Path("id") Long userId);
@Query - 查询参数
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 查询参数注解
* 用于添加URL查询参数
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
/**
* 参数名称
*/
String value();
/**
* 是否编码参数值
*/
boolean encoded() default false;
}
使用示例:
java
@GET("/users")
List<User> searchUsers(@Query("name") String name, @Query("age") Integer age);
@Body - 请求体
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 请求体注解
* 用于标识请求体参数
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Body {
// 请求体注解不需要额外参数
}
使用示例:
java
@POST("/users")
User createUser(@Body User user);
@Header - 请求头
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 请求头注解
* 可以用于方法(静态头)或参数(动态头)
*/
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Header {
/**
* 请求头名称或完整的头信息(name: value格式)
*/
String value();
}
使用示例:
java
// 静态请求头
@POST("/users")
@Header("Content-Type: application/json")
User createUser(@Body User user);
// 动态请求头
@GET("/users")
List<User> getUsers(@Header("Authorization") String token);
4. 扩展注解
@Async - 异步执行
java
package io.github.nemoob.httpclient.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 异步执行注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Async {
/**
* 执行器名称
*/
String executor() default "";
}
@Interceptor - 拦截器
java
package io.github.nemoob.httpclient.annotation;
import io.github.nemoob.httpclient.RequestInterceptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 拦截器注解
* 用于类级别的拦截器配置
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Interceptor {
/**
* 拦截器类数组
*/
Class<? extends RequestInterceptor>[] value();
}
@MethodInterceptor - 方法级拦截器
java
package io.github.nemoob.httpclient.annotation;
import io.github.nemoob.httpclient.RequestInterceptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 方法级拦截器注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodInterceptor {
/**
* 拦截器类数组
*/
Class<? extends RequestInterceptor>[] value();
}
注解解析机制
1. 注解信息提取
我们需要在运行时解析这些注解信息。以下是核心的解析逻辑:
java
public class AnnotationParser {
/**
* 解析HTTP方法注解
*/
public static HttpMethod getHttpMethod(Method method) {
if (method.isAnnotationPresent(GET.class)) {
return HttpMethod.GET;
} else if (method.isAnnotationPresent(POST.class)) {
return HttpMethod.POST;
} else if (method.isAnnotationPresent(PUT.class)) {
return HttpMethod.PUT;
} else if (method.isAnnotationPresent(DELETE.class)) {
return HttpMethod.DELETE;
}
return null;
}
/**
* 获取请求路径
*/
public static String getRequestPath(Method method) {
if (method.isAnnotationPresent(GET.class)) {
return method.getAnnotation(GET.class).value();
} else if (method.isAnnotationPresent(POST.class)) {
return method.getAnnotation(POST.class).value();
}
// ... 其他HTTP方法
return null;
}
/**
* 解析方法参数注解
*/
public static ParameterInfo[] parseParameters(Method method) {
Parameter[] parameters = method.getParameters();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
ParameterInfo[] parameterInfos = new ParameterInfo[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterInfos[i] = parseParameter(parameters[i], parameterAnnotations[i]);
}
return parameterInfos;
}
private static ParameterInfo parseParameter(Parameter parameter, Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof Path) {
return new ParameterInfo(ParameterType.PATH, ((Path) annotation).value());
} else if (annotation instanceof Query) {
return new ParameterInfo(ParameterType.QUERY, ((Query) annotation).value());
} else if (annotation instanceof Body) {
return new ParameterInfo(ParameterType.BODY, null);
} else if (annotation instanceof Header) {
return new ParameterInfo(ParameterType.HEADER, ((Header) annotation).value());
}
}
throw new IllegalArgumentException("Parameter must be annotated with @Path, @Query, @Body, or @Header");
}
}
2. 参数信息封装
为了便于处理,我们定义参数信息的封装类:
java
public class ParameterInfo {
private final ParameterType type;
private final String name;
public ParameterInfo(ParameterType type, String name) {
this.type = type;
this.name = name;
}
// getters...
}
public enum ParameterType {
PATH, // 路径参数
QUERY, // 查询参数
BODY, // 请求体
HEADER // 请求头
}
注解验证机制
为了提供更好的用户体验,我们需要在创建客户端时验证注解的正确性:
java
public class AnnotationValidator {
/**
* 验证接口定义的正确性
*/
public static void validateInterface(Class<?> interfaceClass) {
// 1. 验证接口必须有@HttpClient注解
if (!interfaceClass.isAnnotationPresent(HttpClient.class)) {
throw new IllegalArgumentException("Interface must be annotated with @HttpClient");
}
// 2. 验证接口必须是接口类型
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("@HttpClient can only be applied to interfaces");
}
// 3. 验证每个方法
for (Method method : interfaceClass.getDeclaredMethods()) {
validateMethod(method);
}
}
private static void validateMethod(Method method) {
// 1. 验证方法必须有HTTP方法注解
if (getHttpMethod(method) == null) {
throw new IllegalArgumentException("Method " + method.getName() + " must be annotated with HTTP method annotation");
}
// 2. 验证参数注解
validateParameters(method);
// 3. 验证返回类型
validateReturnType(method);
}
private static void validateParameters(Method method) {
Parameter[] parameters = method.getParameters();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int bodyCount = 0;
for (int i = 0; i < parameters.length; i++) {
boolean hasAnnotation = false;
for (Annotation annotation : parameterAnnotations[i]) {
if (annotation instanceof Path ||
annotation instanceof Query ||
annotation instanceof Header) {
hasAnnotation = true;
break;
} else if (annotation instanceof Body) {
hasAnnotation = true;
bodyCount++;
break;
}
}
if (!hasAnnotation) {
throw new IllegalArgumentException("Parameter " + i + " of method " + method.getName() + " must be annotated");
}
}
// 验证最多只能有一个@Body参数
if (bodyCount > 1) {
throw new IllegalArgumentException("Method " + method.getName() + " can have at most one @Body parameter");
}
}
private static void validateReturnType(Method method) {
Class<?> returnType = method.getReturnType();
// 如果是异步方法,返回类型必须是CompletableFuture
if (method.isAnnotationPresent(Async.class)) {
if (!CompletableFuture.class.isAssignableFrom(returnType)) {
throw new IllegalArgumentException("Async method " + method.getName() + " must return CompletableFuture");
}
}
}
}
使用示例
让我们通过一个完整的示例来看看注解系统的使用:
java
@HttpClient("https://api.example.com")
@Interceptor({LoggingInterceptor.class, AuthInterceptor.class})
public interface UserService {
// 简单的GET请求
@GET("/users")
List<User> getUsers();
// 带路径参数的GET请求
@GET("/users/{id}")
User getUser(@Path("id") Long id);
// 带查询参数的GET请求
@GET("/users")
List<User> searchUsers(@Query("name") String name,
@Query("age") Integer age);
// POST请求带请求体
@POST("/users")
@Header("Content-Type: application/json")
User createUser(@Body User user);
// 带动态请求头的请求
@GET("/users/profile")
User getProfile(@Header("Authorization") String token);
// 异步请求
@GET("/users")
@Async(executor = "userServiceExecutor")
CompletableFuture<List<User>> getUsersAsync();
// 方法级拦截器
@DELETE("/users/{id}")
@MethodInterceptor(AuditInterceptor.class)
void deleteUser(@Path("id") Long id);
}
注解系统的优势
通过精心设计的注解系统,我们实现了:
1. 声明式编程
用户只需要声明接口,无需编写实现代码:
java
// 传统方式
public class UserServiceImpl {
public User getUser(Long id) {
// 大量的HTTP调用代码
HttpURLConnection conn = ...
// 设置请求头、参数等
// 发送请求
// 解析响应
// 异常处理
return user;
}
}
// 使用注解
@HttpClient("https://api.example.com")
public interface UserService {
@GET("/users/{id}")
User getUser(@Path("id") Long id);
}
2. 类型安全
编译时就能发现类型错误:
java
// 编译时错误:参数类型不匹配
@GET("/users/{id}")
User getUser(@Path("id") String id); // 应该是Long类型
3. 自文档化
接口定义即文档,一目了然:
java
@GET("/users/{id}") // 清楚地表明这是获取用户的接口
User getUser(@Path("id") Long id);
4. 易于测试
可以轻松创建 Mock 实现:
java
// 测试时的Mock实现
UserService mockUserService = Mockito.mock(UserService.class);
when(mockUserService.getUser(1L)).thenReturn(testUser);
总结
本文详细介绍了 Atlas HTTP Client 框架的核心注解系统设计与实现。关键要点包括:
- 语义化设计:注解名称直观易懂
- 功能完整:覆盖HTTP协议的主要特性
- 类型安全:编译时验证,减少运行时错误
- 扩展性强:支持拦截器、异步等高级功能
- 验证机制:提供完善的注解验证
在下一篇文章中,我们将介绍如何使用动态代理机制来解析这些注解,并实现实际的HTTP请求处理逻辑。