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.
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/
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.