手把手教你写 httpclient 框架(二)- 核心注解系统设计与实现

前言

在上一篇文章中,我们从整体架构的角度分析了 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 "";
}

设计要点:

  1. value() 和 baseUrl() 的设计:提供两种方式指定基础 URL,value() 是简化写法
  2. 超时配置:提供连接和读取两种超时配置,满足不同场景需求
  3. 异步支持:全局异步配置,简化异步接口的定义
  4. 执行器配置:支持自定义线程池

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 注解。

设计要点:

  1. 统一的设计模式:所有 HTTP 方法注解都有相同的结构
  2. 路径参数:value() 参数用于指定请求路径
  3. 方法级别:只能用于方法上,不能用于类或字段

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 框架的核心注解系统设计与实现。关键要点包括:

  1. 语义化设计:注解名称直观易懂
  2. 功能完整:覆盖HTTP协议的主要特性
  3. 类型安全:编译时验证,减少运行时错误
  4. 扩展性强:支持拦截器、异步等高级功能
  5. 验证机制:提供完善的注解验证

在下一篇文章中,我们将介绍如何使用动态代理机制来解析这些注解,并实现实际的HTTP请求处理逻辑。

相关推荐
vker3 小时前
第 2 天:工厂方法模式(Factory Method Pattern)—— 创建型模式
java·后端·设计模式
准时睡觉3 小时前
SpringSecurity的使用
java·后端
绝无仅有3 小时前
面试经验之mysql高级问答深度解析
后端·面试·github
绝无仅有3 小时前
Java技术复试面试:全面解析
后端·面试·github
对不起初见3 小时前
如何在后端优雅地生成并传递动态错误提示?
java·spring boot
tingyu3 小时前
JAXB 版本冲突踩坑记:SPI 项目中的 XML 处理方案升级
java
NightDW3 小时前
amqp-client源码解析1:数据格式
java·后端·rabbitmq
程序员清风3 小时前
美团二面:KAFKA能保证顺序读顺序写吗?
java·后端·面试
ytadpole18 小时前
揭秘xxl-job:从高可用到调度一致性
java·后端