Backend Development 16 min read

Design and Implementation of an Annotation‑Based HTTP Client in Spring

This article presents a design pattern for a unified, annotation‑driven HTTP client in a Spring‑based payment system, detailing custom annotations, dynamic proxy enhancement, and bean registration via FactoryBean and ImportBeanDefinitionRegistrar, with complete code examples and a summary of key concepts.

Zhuanzhuan Tech
Zhuanzhuan Tech
Zhuanzhuan Tech
Design and Implementation of an Annotation‑Based HTTP Client in Spring

1. Background

Zhuanzhuan's payment center integrates with multiple third‑party payment platforms (WeChat Pay, Alipay, etc.) and needs to interact with them via HTTP for operations such as acquisition, payout, and refund. Existing HttpUtil utilities are fragmented, leading to inconsistent designs, high coupling, and low reusability.

To address these issues, the team built a unified, annotation‑driven HTTP client that defines a design contract for all HTTP interactions with third‑party channels.

2. Practice Ideas

2.1 Custom Annotations

Goal:

Attach common parameter information to interfaces via custom annotations, making the interface itself serve as documentation.

When adding new methods, simple configuration based on the documented interface is sufficient.

Reduce boilerplate code and keep interface definitions concise.

2.2 Dynamic Proxy to Enhance Interface Methods

Goal:

Use dynamic proxies to hide complex or divergent implementation details, allowing users to program against pure interfaces.

Combine with annotations for non‑intrusive code extension.

2.3 Inject Proxy Bean into Spring Container

Goal:

Support Spring IOC features.

Ensure that the proxy implementation can be used exactly like a normal bean, without the user noticing any difference.

3. Implementation

Overall Process:

During Spring startup, @Import({HTTPMethodScannerRegistrar.class}) triggers a custom ImportBeanDefinitionRegistrar to register beans.

The registrar scans for interfaces annotated with @HTTPController , extracts their metadata, and creates a HTTPControllerFactoryBean definition, which is then registered in the Spring container.

The HTTPControllerFactoryBean uses Proxy.newProxyInstance to generate a dynamic proxy that implements the target interface.

3.1 Custom Annotations

HTTPController Annotation

Runtime‑retained annotation applied to a class or interface to mark it as a third‑party HTTP gateway.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface HTTPController {
    // Third‑party description
    String desc() default "";
    // Third‑party enum
    ThirdPartEnum thirdPart();
    // Base URL
    String baseUrl() default "";
    // Invocation handler class
    Class
invocationHandlerClass();
}

HTTPMethod Annotation

Runtime‑retained annotation applied to a method to describe the specific HTTP endpoint.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface HTTPMethod {
    // Request path
    String url();
    // HTTP request type, default POST
    HTTPRequestType requestType() default HTTPRequestType.POST;
    // Retry count
    int retryCount() default 0;
    enum HTTPRequestType { GET, POST }
}

Usage Example

@HTTPController(desc = "WeChat Pay",
                thirdPart = ThirdPartEnum.WeiXinPay,
                baseUrl = "https://api.mch.weixin.qq.com",
                invocationHandlerClass = WeiXinPayInvocationHandler.class)
public interface WeiXinPayRequestGateway {
    @HTTPMethod(url = "/ea/pCustomerReg.action", requestType = HTTPRequestType.POST, retryCount = 2)
    ThirdPartResponse
pCustomerReg(CustomerRegV2Request request);

    @HTTPMethod(url = "/ea/transfer", requestType = HTTPRequestType.POST)
    ThirdPartResponse
transfer(TransferRequest request);

    @HTTPMethod(url = "/ea/transferQuery", requestType = HTTPRequestType.GET, retryCount = 2)
    ThirdPartResponse
transferQuery(TransferQueryRequest request);
}

3.2 Dynamic Proxy Enhancement

The following handler implements the core HTTP request logic for the WeChat Pay channel using JDK dynamic proxy.

@Slf4j
public class WeiXinPayInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        WeiXinPayBaseResponse response = null;
        try {
            // Http processing logic
            response = realLogic(method, args[0]);
        } catch (Exception ex) {
            // Request failure handling
            return ThirdPartResponse.of(ThirdPartTransferResultEnum.UNCLEAR_FAILURE);
        }
        // Return result
        return ThirdPartResponse.of(response);
    }

    private WeiXinPayBaseResponse realLogic(Method method, Object args) {
        // Retrieve method annotation
        HTTPMethod httpMethod = method.getAnnotation(HTTPMethod.class);
        // Build HttpOptions based on retry count
        HttpOptions httpOptions = httpOptionsBuild(httpMethod.retryCount());
        // Build request body from URL and method arguments
        HttpRequest httpRequest = httpRequestBuild(httpMethod.url(), (WeiXinPayBaseRequest) args);
        // Determine request type
        HTTPRequestType httpRequestType = httpMethod.requestType();
        // Execute request
        HttpResponse httpResponse = executeHttpRequest(httpOptions, httpRequest, httpRequestType);
        // Resolve generic return type
        Type genericReturnType = method.getGenericReturnType();
        if (genericReturnType instanceof ParameterizedType) {
            Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
            genericReturnType = actualTypeArguments[0];
        }
        // Decode and deserialize response
        String decodedData = DecodeUtil.decode(httpResponse.getResult());
        return GsonUtil.fromJson(decodedData, genericReturnType);
    }
    // ... httpRequestBuild, executeHttpRequest, etc.
}

3.3 Injecting Proxy Bean into Spring

The solution uses FactoryBean together with ImportBeanDefinitionRegistrar to register the proxy as a Spring bean.

FactoryBean Overview

A simple demo shows how a FactoryBean can produce a custom object that is later retrieved from the container.

public interface Person { void sayHello(); }

@Setter
public class XiaoMing implements FactoryBean
, Person {
    private String regards;
    @Override public Object getObject() { return new ZhangSan(regards); }
    @Override public Class
getObjectType() { return ZhangSan.class; }
    @Override public void sayHello() { System.out.println("Greetings from XiaoMing: " + regards); }
}

public class ZhangSan implements Person { String regards; public ZhangSan(String r){ this.regards=r; }
    @Override public void sayHello(){ System.out.println("Greetings from ZhangSan: " + regards); } }

public class BeanDefinitionBuilderExample {
    public static void main(String[] args){
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(XiaoMing.class).getBeanDefinition();
        beanDefinition.getPropertyValues().add("regards", "Hello World");
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        beanFactory.registerBeanDefinition("person", beanDefinition);
        Person bean = (Person) beanFactory.getBean("person");
        bean.sayHello();
    }
}

The example demonstrates that the bean retrieved from the factory is actually the object returned by FactoryBean#getObject() .

HTTPControllerFactoryBean Implementation

@Setter
@Slf4j
public class HTTPControllerFactoryBean implements FactoryBean
{
    private Class
targetClass;
    private ThirdPartEnum thirdPart;
    private String baseUrl;
    private Class
invocationHandlerClass;

    @Override public Object getObject() {
        InvocationHandler invocationHandler;
        try { invocationHandler = invocationHandlerClass.newInstance(); }
        catch (Exception e) { throw new RuntimeException("[HTTPControllerFactoryBean‑invocationHandlerClass‑newInstance] error", e); }
        return Proxy.newProxyInstance(HTTPControllerFactoryBean.class.getClassLoader(), new Class[]{targetClass}, invocationHandler);
    }
    @Override public Class
getObjectType() { return targetClass; }
    @Override public boolean isSingleton() { return true; }
}

HTTPMethodScannerRegistrar Implementation

The registrar scans a given package for interfaces annotated with @HTTPController and registers a corresponding HTTPControllerFactoryBean for each.

public class HTTPMethodScannerRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false) {
            @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                if (beanDefinition.getMetadata().isInterface()) {
                    try { return beanDefinition.getMetadata().hasAnnotatedMethods(HTTPController.class.getName()); }
                    catch (Exception ex) { throw new RuntimeException("[isCandidateComponent error]", ex); }
                }
                return false;
            }
        };
        Set
candidates = scanner.findCandidateComponents("com.zhuanzhuan.zzpaycore.gateway");
        for (BeanDefinition bd : candidates) {
            if (bd instanceof AnnotatedBeanDefinition) {
                registerBeanDefinition((AnnotatedBeanDefinition) bd, registry);
            }
        }
    }

    private void registerBeanDefinition(AnnotatedBeanDefinition beanDefinition, BeanDefinitionRegistry registry) {
        AnnotationMetadata metadata = beanDefinition.getMetadata();
        String className = metadata.getClassName();
        AbstractBeanDefinition factoryBeanDef = BeanDefinitionBuilder.genericBeanDefinition(HTTPControllerFactoryBean.class).getBeanDefinition();
        AnnotationAttributes attrs = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(HTTPController.class.getName()));
        factoryBeanDef.getPropertyValues().add("targetClass", className);
        factoryBeanDef.getPropertyValues().add("baseUrl", attrs.getString("baseUrl"));
        factoryBeanDef.getPropertyValues().add("thirdPart", attrs.get("thirdPart"));
        factoryBeanDef.getPropertyValues().add("invocationHandlerClass", attrs.get("invocationHandlerClass"));
        registry.registerBeanDefinition(className, factoryBeanDef);
    }
}

Spring Initialization

Adding @Import({HTTPMethodScannerRegistrar.class}) to the main application class triggers the whole registration process.

// Use @Import to load the registrar
@Import({HTTPMethodScannerRegistrar.class})
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

4. Summary

The annotation‑driven HTTP client is built on three pillars: custom annotations, JDK dynamic proxy (or CGLIB), and Spring's bean post‑processor mechanism. It provides a clean, reusable, and low‑coupling way to interact with heterogeneous third‑party payment APIs.

Key takeaways include:

Designing custom annotations and annotation processors for Spring‑driven development.

Applying JDK dynamic proxy or CGLIB for method interception.

Understanding FactoryBean vs. BeanFactory, Spring bean lifecycle, and post‑processor extensions.

5. References

https://blog.51cto.com/u_15162069/2820375

https://developpaper.com/beanfactory-and-factorybean-in-spring-is-enough/

JavaSpringAnnotationsFactoryBeanDynamic ProxyHTTP Client
Zhuanzhuan Tech
Written by

Zhuanzhuan Tech

A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.