Java 动态代理详解
1. 动态代理是什么?
动态代理是一种在运行时动态生成代理类的技术,允许在不修改原始类代码的前提下,通过代理对象增强原始对象的行为(如添加日志、事务、权限控制等)。
核心特点:
- 动态性:代理类在运行时生成,无需手动编写。
- 解耦性:将增强逻辑与业务逻辑分离,符合开闭原则。
- 灵活性:可代理任意接口,支持多种增强场景。
2. 为什么需要动态代理?
核心需求:
- 代码复用:避免为每个类手动编写静态代理类。
- 横切关注点:统一处理日志、事务、权限等公共逻辑(AOP 思想)。
- 解耦业务逻辑:业务代码专注于核心功能,增强逻辑由代理处理。
- 动态扩展:运行时按需生成代理,适应复杂场景(如远程调用、延迟加载)。
3. 动态代理的底层实现
Java 原生动态代理基于以下两个核心类:
java.lang.reflect.Proxy
:生成代理类的工厂类。java.lang.reflect.InvocationHandler
:代理逻辑的处理器接口。
实现步骤:
-
定义接口:代理类需实现目标接口。
public interface UserService { void saveUser(String name); }
-
实现目标类:原始业务逻辑。
public class UserServiceImpl implements UserService { @Override public void saveUser(String name) { System.out.println("保存用户:" + name); } }
-
实现
InvocationHandler
:定义增强逻辑。public class LogInvocationHandler implements InvocationHandler { private final Object target; // 被代理对象 public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法调用前:" + method.getName()); Object result = method.invoke(target, args); // 调用原始方法 System.out.println("方法调用后:" + method.getName()); return result; } }
-
生成代理对象:通过
Proxy.newProxyInstance
动态创建代理。UserService userService = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), new LogInvocationHandler(userService) ); proxy.saveUser("张三"); // 调用代理方法
底层原理:
- 动态生成代理类:运行时通过字节码技术生成
$Proxy0
类,继承Proxy
并实现目标接口。 - 方法调用链路:代理类方法调用 →
InvocationHandler.invoke()
→ 原始方法。
4. 动态代理的应用场景
场景 | 说明 |
---|---|
AOP(面向切面编程) | 统一处理日志、事务、权限等横切逻辑(如 Spring AOP)。 |
远程方法调用(RPC) | 动态代理封装网络通信细节(如 Dubbo、Feign)。 |
延迟加载 | 代理对象按需加载资源(如 Hibernate 的延迟加载)。 |
接口适配 | 将第三方接口适配为统一接口(如 MyBatis 的 Mapper 接口)。 |
Mock 测试 | 动态生成测试替身,模拟外部依赖行为。 |
5. 主流框架中的动态代理
示例1:Spring AOP
- 场景:为业务方法添加事务管理。
- 实现:通过
JdkDynamicAopProxy
或CglibAopProxy
生成代理。
Spring 在运行时生成代理对象,拦截@Service public class OrderService { @Transactional public void createOrder() { // 业务逻辑 } }
@Transactional
注解的方法,自动开启和提交事务。
示例2:MyBatis Mapper 接口
- 场景:将 Java 接口映射为 SQL 操作。
- 实现:通过
MapperProxy
动态代理接口方法。
MyBatis 动态代理public interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User getUserById(int id); }
UserMapper
,将getUserById
方法调用转换为执行 SQL 查询。
示例3:Feign 声明式 HTTP 客户端
- 场景:定义 HTTP 请求接口,无需手动编写 HTTP 调用代码。
- 实现:通过
FeignInvocationHandler
动态代理接口。
Feign 代理@FeignClient(name = "user-service") public interface UserApi { @GetMapping("/users/{id}") User getUser(@PathVariable("id") int id); }
UserApi
,将方法调用转换为 HTTP 请求。
6. 使用注意事项
- 仅支持接口代理:JDK 动态代理要求目标类必须实现接口,若需代理类,需使用 CGLIB。
- 性能开销:反射调用和动态生成类会带来轻微性能损耗,高频调用场景需谨慎。
equals
和hashCode
:代理对象的equals
和hashCode
方法可能不符合预期,需特殊处理。- 调试复杂性:动态生成的代理类名包含
$Proxy
,调试时需注意。 - 循环依赖:Spring 中动态代理可能导致循环依赖问题,需通过
@Lazy
或构造函数注入解决。
7. 示例:实现一个极简版的 Feign
为了理解动态代理的实际应用,我们来实现一个极简版的 Feign
客户端(类似声明式 HTTP 请求工具),通过动态代理将接口方法自动转换为 HTTP 请求。以下是完整代码示例和详细注释:
-
定义 HTTP 请求注解
我们需要自定义注解来标记接口方法和参数,类似于 Feign 的@GetMapping
、@PathVarible
:// 定义 HTTP 请求方法注解(类似 @GetMapping) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyRequest { String method() default "GET"; // 请求方法 String url(); // 请求路径(例如 "/users/{id}") } // 定义路径变量注解(类似 @PathVariable) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface MyPathVar { String value(); // 路径变量名(例如 "id") } // 定义查询参数注解(类似 @RequestParam) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface MyQueryParam { String value(); // 查询参数名(例如 "name") }
-
编写动态代理核心类
创建一个MyFeignClient
工厂类,通过动态代理将接口方法转换为 HTTP 请求:import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; public class MyFeignClient { // 创建代理对象(类似 Feign 的动态代理) public static <T> T create(Class<T> clazz, String baseUrl) { return (T) Proxy.newProxyInstance( clazz.getClassLoader(), new Class[]{clazz}, new MyFeignInvocationHandler(baseUrl) ); } // 代理逻辑处理器 private static class MyFeignInvocationHandler implements InvocationHandler { private final String baseUrl; public MyFeignInvocationHandler(String baseUrl) { this.baseUrl = baseUrl; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 解析方法上的注解 MyRequest request = method.getAnnotation(MyRequest.class); if (request == null) { throw new RuntimeException("方法必须使用 @MyRequest 注解"); } // 2. 构建请求 URL(替换路径变量) String url = buildUrl(request.url(), method, args); // 3. 发送 HTTP 请求(这里简化为控制台输出) System.out.println("发送 HTTP 请求: " + request.method() + " " + url); // 4. 返回模拟结果(实际应解析 HTTP 响应) return "模拟响应数据"; } // 构建完整 URL(处理路径变量和查询参数) private String buildUrl(String path, Method method, Object[] args) { // 替换路径变量(例如 "/users/{id}" → "/users/123") Map<String, Object> pathVars = new HashMap<>(); Annotation[][] paramAnnotations = method.getParameterAnnotations(); for (int i = 0; i < paramAnnotations.length; i++) { for (Annotation ann : paramAnnotations[i]) { if (ann instanceof MyPathVar) { String varName = ((MyPathVar) ann).value(); pathVars.put(varName, args[i]); } } } for (Map.Entry<String, Object> entry : pathVars.entrySet()) { path = path.replace("{" + entry.getKey() + "}", entry.getValue().toString()); } // 拼接查询参数(例如 "?name=Alice") StringBuilder queryParams = new StringBuilder(); for (int i = 0; i < paramAnnotations.length; i++) { for (Annotation ann : paramAnnotations[i]) { if (ann instanceof MyQueryParam) { String paramName = ((MyQueryParam) ann).value(); if (queryParams.length() == 0) { queryParams.append("?"); } else { queryParams.append("&"); } queryParams.append(paramName).append("=").append(args[i]); } } } return baseUrl + path + queryParams; } } }
-
定义业务接口并使用代理
假设我们要调用一个用户服务接口,定义如下:public interface UserApi { // 定义接口方法(类似 Feign 的声明式接口) @MyRequest(method = "GET", url = "/users/{id}") String getUserById(@MyPathVar("id") int userId); @MyRequest(method = "GET", url = "/users") String getUsersByRole(@MyQueryParam("role") String role); }
使用动态代理发送 HTTP 请求:
public class Main { public static void main(String[] args) { // 创建代理对象(类似 Feign 的自动注入) UserApi userApi = MyFeignClient.create(UserApi.class, "http://api.example.com"); // 调用方法 → 自动转换为 HTTP 请求 String user = userApi.getUserById(123); System.out.println("响应结果: " + user); String users = userApi.getUsersByRole("admin"); System.out.println("响应结果: " + users); } }
-
运行结果
控制台将输出:发送 HTTP 请求: GET http://api.example.com/users/123 响应结果: 模拟响应数据 发送 HTTP 请求: GET http://api.example.com/users?role=admin 响应结果: 模拟响应数据
关键点解析
1. 动态代理如何工作?
- 当调用
userApi.getUserById(123)
时,实际调用的是代理对象的方法。 - 代理对象通过
InvocationHandler.invoke()
拦截方法调用,解析注解并构造 HTTP 请求。
2. 注解处理流程
- 路径变量替换:扫描方法参数的
@MyPathVar
注解,将{id}
替换为实际值。 - 查询参数拼接:扫描
@MyQueryParam
注解,将参数拼接为?key=value
形式。
3. 与实际 Feign 的区别
- 简化网络请求:真实 Feign 会处理连接池、编解码、异常重试等。
- 注解支持:真实 Feign 支持更多注解(如
@RequestBody
、@Headers
)。 - 集成组件:真实 Feign 与 Ribbon、Hystrix 等结合实现负载均衡和熔断。
如何扩展这个示例?
- 实现真实 HTTP 请求:替换
System.out.println
,使用HttpClient
或OkHttp
发送请求。 - 支持 JSON 解析:将响应体反序列化为 Java 对象。
- 添加超时和重试:在
InvocationHandler
中增加网络处理逻辑。 - 支持其他注解:如
@MyPostMapping
、@MyRequestBody
。
希望这个示例能让你直观理解动态代理的实战用法!
总结
动态代理通过运行时生成代理类,实现代码增强和解耦,是 Java 高级编程和框架设计的核心技术。它在 AOP、RPC、ORM 等场景中广泛应用,如 Spring、MyBatis、Feign 等框架均依赖动态代理实现核心功能。使用时需注意接口限制、性能影响及代理对象的特殊性,合理选择代理方式(JDK Proxy 或 CGLIB)以适配业务需求。