设计模式之代理模式

0 阅读10分钟

引言

在生活中,我们经常能见到代理模式的身影,比如商品代购、房屋中介。

在软件开发中,代理模式为其他对象提供一种代理,以控制对这个对象的访问。代理类负责增强控制对目标对象的访问,而核心业务逻辑仍然由被代理对象完成。这种增强常用于日志记录、权限校验、事务管理或性能监控。

代理模式主要分为三种实现方式

  • 静态代理:在编译阶段就确定了代理关系,需要为每个被代理类手动编写一个代理类。
  • JDK动态代理:利用Java反射机制,在运行时动态为接口的创建代理对象
  • Cglib动态代理:基于字节码技术,通过创建被代理类的子类来实现代理,不要求目标类实现接口。

用户服务调用

我们以一个简单的用户服务(UserService)为例,来逐步讲解代理模式的应用。其类图如下:

image-20260327001358410.png

Client: 客户端,需要使用用户服务的调用方。

UserService: 用户服务接口,定义业务主要方法。

UserServiceImpl: 业务实现类。

基础代码示例

首先,我们定义服务接口和实现类,作为后续所有代理模式演示的基础。

用户服务接口

// UserService.java
public interface UserService {
​
    User getUserById(Integer id);
​
    Boolean addUser(User user);
}
​

用户服务实现类

为简化示例,我们只打印日志模拟业务操作。

// UserServiceImpl.java
@Slf4j
public class UserServiceImpl implements UserService {
    @Override
    public User getUserById(Integer id) {
        System.out.println("UserServiceImpl 查询用户");
        return new User();
    }
​
    @Override
    public Boolean addUser(User user) {
        System.out.println("UserServiceImpl 添加用户");
        return true;
    }
}

客户端使用

非常简单的调用示例

public static void main(String[] args) throws Throwable {
    // 正常的客户端调用
    System.out.println("===============正常调用 UserServiceImpl 方法===================");
    UserService userService = new UserServiceImpl();
    userService.getUserById(1);
    System.out.println();
}

运行结果

===============正常调用 UserServiceImpl 方法===================
UserServiceImpl 查询用户

例子作为引子,也是我们日常开发中经常用到的核心示例

代理模式类图

image-20260327001818891.png

除了普通用户服务调用相关组件外,代理模式新增了UserServiceProxy这个代理类。其有两个特征:

  1. 实现了与目标对象相同的接口 UserService, 保证了对外的行为一致性。
  2. 持有被代理对象 UserServiceImpl的引用,以便在执行增强逻辑后可调用真正的业务方法。

引入代理类后,Client 不再直接与 UserServiceImpl 交互,而是通过 UserServiceProxy 来间接调用。这使得 UserServiceImpl 可以更关注业务逻辑本身。

静态代理

静态代理是最简单、直观的实现方式,但缺点也很明显:每个被代理类都需要一个对应的代理类,当系统规模变大时,会产生大量的冗余代码。

静态代理类

@Slf4j
public class UserServiceProxy implements UserService {
​
    // 持有被代理对象的引用
    private final UserService userService;
​
    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }
​
    @Override
    public User getUserById(Integer id) {
        System.out.println("类 UserServiceProxy, 方法 -> getUserById, 参数 -> " + id);
        // 调用被代理对象的真实业务逻辑
        User user = userService.getUserById(id);
        System.out.println("类 UserServiceProxy, 方法 -> getUserById 执行结束");
        return user;
    }
​
    @Override
    public Boolean addUser(User user) {
        System.out.println("类 UserServiceProxy, 方法 -> addUser, 参数 -> " + user);
        Boolean result = userService.addUser(user);
        System.out.println("类 UserServiceProxy, 方法 -> addUser 执行结束");
        return result;
    }
}

客户端使用静态代理

// 这里使用静态代理对象
UserService userServiceProxy = new UserServiceProxy(userService);
userServiceProxy.getUserById(2);

运行结果

===============使用静态代理调用===================
类 UserServiceProxy, 方法 -> getUserById, 参数 -> 2
UserServiceImpl 查询用户
类 UserServiceProxy, 方法 -> getUserById 执行结束

JDK动态代理

为了解决静态代理的冗余问题,JDK提供了动态代理机制。它可以在运行时动态创建代理对象,但是要求被代理的类必须实现至少一个接口

JDK动态代理主要涉及两个核心组件:

  • Proxy: 提供 newProxyInstance 静态方法,用于动态创建代理对象。
  • InvocationHandler接口: 我们需要实现这个接口,编写具体的增强逻辑。

日志处理代理类

下面我们实现一个通用的日志处理类,它可以在任意方法执行前后打印日志

public class LogHandler implements InvocationHandler {
​
    // 持有被代理对象
    private final Object target;
​
    // 在构造方法时传入,即创建后表明对象代理了目标类
    public LogHandler(Object target) {
        this.target = target;
    }
​
    // 重点, 其中proxy是代理对象,method为执行的方法, args为调用方法时的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 打印方法名,参数等信息
        System.out.println("代理类 -> LogHandler, 方法名 -> " + method.getName());
        if (args != null) {
            // 打印 args 参数
            for (Object arg : args) {
                System.out.println("代理类 -> LogHandler, 参数 -> " + arg);
            }
        }
        // 通过 method.invoke 来执行被代理对象的方法。
        Object result = method.invoke(target, args);
        // 被代理对象方法执行结束后代理类处理的事。
        System.out.println("代理类 -> LogHandler, 方法名 -> " + method.getName() + " 执行结束");
        return result;
    }
}
​

方法耗时统计代理类

为了展示动态代理的灵活性,我们再实现一个通用的耗时统计类。

public class TimeCostHandler implements InvocationHandler {
​
    private final Object target;
​
    public TimeCostHandler(Object target) {
        this.target = target;
    }
​
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        long end = System.currentTimeMillis();
        Object result = method.invoke(target, args);
        System.out.println("代理类 -> TimeCostHandler, 方法 -> " + method.getName() + " 耗时 -> " + (end - start) + " 毫秒");
        return result;
    }
}
​

客户端使用JDK动态代理

System.out.println("===============使用JDK动态代理调用===================");
UserService userServiceLogProxy= (UserService)Proxy.newProxyInstance(
    userService.getClass().getClassLoader(),
    userService.getClass().getInterfaces(),
    new LogHandler(userService));
userServiceLogProxy.getUserById(3);
System.out.println();
​
System.out.println("===============使用JDK动态代理复合调用1===================");
UserService composeProxy1= (UserService)Proxy.newProxyInstance(
    userService.getClass().getClassLoader(),
    userService.getClass().getInterfaces(),
    // 注意这里是先运行UserServiceLogProxy,再运行 TimeCostHandler
    new TimeCostHandler(userServiceLogProxy));
// timeCostProxy 代理了 LogProxy
composeProxy1.getUserById(4);
System.out.println();
​
System.out.println("===============使用JDK动态代理复合调用2===================");
// 先构建TimeCostHandler
UserService userServiceTimeCostProxy = (UserService)Proxy.newProxyInstance(
    userService.getClass().getClassLoader(),
    userService.getClass().getInterfaces(),
    new TimeCostHandler(userService)
);
UserService composeProxy2= (UserService)Proxy.newProxyInstance(
    userService.getClass().getClassLoader(),
    userService.getClass().getInterfaces(),
    // 注意这里是先运行TimeCostHandler,再运行 UserServiceLogProxy
    new LogHandler(userServiceTimeCostProxy));
composeProxy2.getUserById(5);
System.out.println();

上面代码实现了JDK动态代理,以及复合动态代理调用。

运行结果

可以重点观察复合调用时顺序以及输出的区别

​
===============使用JDK动态代理调用===================
代理类 -> LogHandler, 方法名 -> getUserById
代理类 -> LogHandler, 参数 -> 3
UserServiceImpl 查询用户
代理类 -> LogHandler, 方法名 -> getUserById 执行结束
​
===============使用JDK动态代理复合调用1===================
代理类 -> LogHandler, 方法名 -> getUserById
代理类 -> LogHandler, 参数 -> 4
UserServiceImpl 查询用户
代理类 -> LogHandler, 方法名 -> getUserById 执行结束
代理类 -> TimeCostHandler, 方法 -> getUserById 耗时 -> 0 毫秒
​
===============使用JDK动态代理复合调用2===================
代理类 -> LogHandler, 方法名 -> getUserById
代理类 -> LogHandler, 参数 -> 5
UserServiceImpl 查询用户
代理类 -> TimeCostHandler, 方法 -> getUserById 耗时 -> 0 毫秒
代理类 -> LogHandler, 方法名 -> getUserById 执行结束

Cglib动态代理

Cglib(Code Generation Library)是一个强大的字节码生成库,它通过继承被代理类来创建代理,因此不要求目标类实现接口。Spring AOP在目标类未实现接口时,会默认使用Cglib。

Cglib动态代理主要涉及两个核心组件:

  • MethodInterceptor 接口: 实现此接口增强业务逻辑。
  • Enchancer类: 用于创建代理对象

这里我们依然以日志为例

日志处理代理类

public class LogInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("Cglib代理类 -> LogInterceptor, 方法 -> " + method.getName());
        if (objects != null) {
            // 打印 args 参数
            for (Object arg : objects) {
                System.out.println("Cglib代理类 -> LogInterceptor, 参数 -> " + arg);
            }
        }
        // 通过MethodProxy调用被代理类的方法,性能优于反射
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println("Cglib代理类 -> LogInterceptor, 方法执行结束");
        return result;
    }
}
​

客户端使用Cglib动态代理

// 创建 Enhancer 对象
Enhancer enhancer = new Enhancer();
// 设置被代理的类, 基于实现子类方式实现
enhancer.setSuperclass(UserServiceImpl.class);
// 设置代理类需要做的事, MethodInterceptor实现类
enhancer.setCallback(new LogInterceptor());
// enhancer.create()方法创建出被代理对象。
UserService proxy = (UserService) enhancer.create();
proxy.getUserById(6);
System.out.println();

运行结果

===============使用CGLIB动态代理调用===================
Cglib代理类 -> LogInterceptor, 方法 -> getUserById
Cglib代理类 -> LogInterceptor, 参数 -> 6
UserServiceImpl 查询用户
Cglib代理类 -> LogInterceptor, 方法执行结束

JDK动态代理 VS Cglib动态代理

对比JDK动态代理Cglib动态代理
原理基于反射机制,动态生成实现了接口的代理类基于字节码技术,动态生成目标类的子类作为代理类
性能早期版本性能较低,JDK 1.8后性能大幅提升,两者接近方法调用通过 MethodProxy 转发,效率较高
适用场景被代理类需要实现接口被代理类不能是 final,方法**不能是 finalstatic**
实现接口实现InvocationHandler接口实现 MethodInterceptor接口
代理对象创建Proxy.newProxyInstance 静态方法创建Enhancer对象创建

如何选择代理方式

在实际开发中,可以遵循以下原则:

  • 优先使用 JDK 动态代理:如果目标类实现了接口,优先选择 JDK 动态代理。它是 Java 原生支持,没有额外的依赖,且性能在 JDK 8+ 后已经非常优秀。
  • 必要时使用 Cglib:当目标类没有实现接口,或者需要代理的方法无法通过接口暴露时,使用 Cglib。但需要注意目标类和方法不能是 final 的。
  • Spring AOP 的默认策略:Spring 框架会自动在两者之间切换。如果目标类实现了至少一个接口,默认使用 JDK 动态代理;否则使用 Cglib。你也可以通过配置强制使用 Cglib:spring.aop.proxy-target-class=true

Spring AOP实现动态代理

Spring AOP(面向切面编程)的底层正是基于JDK动态代理和Cglib动态代理实现的。下面通过一个简单的示例,展示如何在Spring Boot中使用AOP。

1、pom.xml引入aop

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2、声明自定义注解

我们定义一个 @MyLog 注解,用于标记需要打印日志的方法。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
​

3、实现切面

创建切面类, 用 @Around通知来增强所有标注了 @MyLog的方法。

@Aspect
// 将切面注册到Spring容器
@Component 
public class MyLogAspect {
    // @Around 注解 表示在方法执行时进行拦截,拦截的方法是 @MyLog 注解的方法
    // 同样还可以用 @Before, @After, @AfterReturning, @AfterThrowing
    @Around("@annotation(org.tech.annotation.MyLog)")
    public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("类 -> " + joinPoint.getTarget().getClass().getName() + " 方法 -> " + joinPoint.getSignature().getName());
        // 执行被代理的方法, 注意参数和返回值。
        Object result = joinPoint.proceed(joinPoint.getArgs());
        System.out.println("类 -> " + joinPoint.getTarget().getClass().getName() + " 方法 -> " + joinPoint.getSignature().getName() + " 执行结束");
        return result;
    }
}
​

4、开启AOP并注册Bean

在启动类上添加 @EnableAspectJAutoProxy 注解开启AOP支持,并注册我们的业务Bean。

@SpringBootApplication
@EnableAspectJAutoProxy
public class TechLabsApplication {
​
    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }
​
    public static void main(String[] args) {
        SpringApplication.run(TechLabsApplication.class, args);
    }
​
}
​

5、使用注解@MyLog

@Slf4j
public class UserServiceImpl implements UserService {
​
    // 这里使用@MyLog注解,被代理的方法会打印日志
    @MyLog
    @Override
    public User getUserById(Integer id) {
        System.out.println("UserServiceImpl 查询用户");
        return new User();
    }
​
    @Override
    public Boolean addUser(User user) {
        System.out.println("UserServiceImpl 添加用户");
        return true;
    }
}
​
​

6、示例

通过实现ApplicationRunner接口在启动后执行方法。

@Component
public class TestApplicationRunner implements ApplicationRunner {
​
    private final UserService userService;
​
    public TestApplicationRunner(UserService userService) {
        this.userService = userService;
    }
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 启动后运行
        userService.getUserById(5);
    }
}
​

7、结果显示

-> org.tech.service.impl.UserServiceImpl 方法 -> getUserById
UserServiceImpl 查询用户
类 -> org.tech.service.impl.UserServiceImpl 方法 -> getUserById 执行结束

动态代理常见场景

动态代理因其灵活性和非侵入性,在现代框架和业务系统中被广泛应用。以下是几个典型的应用场景:

  • 日志记录与监控:在不修改业务代码的前提下,统一记录方法的调用参数、返回值、执行时间等,用于系统监控和问题排查。
  • 事务管理:Spring 通过 @Transactional 注解,利用动态代理在方法执行前后开启、提交或回滚事务,使业务代码专注于数据操作本身。
  • 权限控制:在方法执行前检查当前用户是否拥有调用权限,实现统一的认证和授权逻辑。
  • 缓存管理:在方法执行前先查询缓存,如果命中则直接返回,否则执行目标方法并将结果存入缓存。

总结

本文从生活中的例子切入,详细介绍了代理模式的三种主要实现方式:静态代理、JDK动态代理和Cglib动态代理。并通过一个用户服务的例子,一步步展示了如何从直接调用演进到使用代理增强。最后,通过Spring AOP的示例和常见应用场景,说明了动态代理在实际开发中的重要性。

理解代理模式的原理,不仅能帮助我们更好地使用Spring等主流框架,也能在遇到需要功能增强而又不想侵入业务代码的场景时,提供一种优雅的解决方案。