设计模式-代理模式Proxy

代理模式Proxy

代理模式 (Proxy)

代理设计模式(Proxy Design Pattern)是一种结构型设计模式,它为其他对象提供一个代理,以控制对这个对象的访问。代理模式可以用于实现懒加载、安全访问控制、日志记录等功能。

在设计模式中,代理模式可以分为静态代理和动态代理 。静态代理是指代理类在编译时 就已经确定,而动态代理是指代理类在运行时动态生成

1) 静态代理

1.a) 原理解析

在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

1.b) 使用场景

1.缓存代理

缓存代理通常会在内部维护一个缓存数据结构,如 HashMap 或者 LinkedHashMap,用来存储已经处理过的请求及其结果。

假设有一个数据查询接口,它从数据库或其他数据源中检索数据。在没有缓存代理的情况下,每次查询都需要访问数据库,这可能会导致较高的资源消耗和延迟。通过引入缓存代理,我们可以将查询结果存储在内存中,从而避免重复查询数据库。

java 复制代码
public interface DataQuery {
    String query(String queryKey);
}
java 复制代码
public class DatabaseDataQuery implements DataQuery {
    @Override
    public String query(String queryKey) {
        // 使用数据源从数据库查询数据很慢
        return "result";
    }
}

创建一个缓存代理类,它同样实现了 DataQuery 接口,并在内部使用HashMap 作为缓存:

java 复制代码
public class DatabaseDataQueryProxy implements DataQuery {
    // 实现缓存,需要数据结构
    private Map<String, String> cache = new HashMap<>(256);

    // 你代理谁,就要持有谁
    private DatabaseDataQuery dataQuery;

    public DatabaseDataQueryProxy() {
        // 1.屏蔽被代理对象
        this.dataQuery = new DatabaseDataQuery();
    }

    @Override
    public String query(String queryKey) {
        // 2.对被代理对象的方法做增强
        // 2.1.查询缓存,命中则返回
        String result = cache.get(queryKey);
        if (result != null) {
            System.out.println("命中缓存,走缓存");
            return result;
        }

        // 2.2.未命中,则查询数据库
        result = dataQuery.query(queryKey);
        // 2.2.1.如果有结果,需要将结果保存到缓存中,再返回
        if (result != null) {
            cache.put(queryKey, result);
        }
        System.out.println("未命中,走持久层");
        return result;
    }
}
java 复制代码
// 测试代码
@Test
void test() {
    DataQuery dataQuery = new DatabaseDataQueryProxy();

    String value = dataQuery.query("key1");
    System.out.println(value);

    value = dataQuery.query("key1");
    System.out.println(value);

    value = dataQuery.query("key2");
    System.out.println(value);
}

2.安全代理

用于控制对真实主题对象的访问。通过安全代理,可以实现访问控制、权限验证等安全相关功能。

假设我们有一个敏感数据查询接口,只有具有特定权限的用户才能访问:

3.虚拟代理

在需要时延迟创建耗时或资源密集型对象。虚拟代理在初始访问时才创建实际对象,之后将直接使用该对象。这可以避免在实际对象尚未使用的情况下就创建它,从而节省资源。

以下是一个虚拟代理的应用示例:

假设我们有一个大型图片类,它从网络加载图像。由于图像可能非常大,我们希望在需要显示时才加载它。为了实现这一点,我们可以创建一个虚拟代理来代表大型图片类。

4.远程代理

用于访问位于不同地址空间的对象。远程代理可以为本地对象提供与远程对象相同的接口,使得客户端可以透明地访问远程对象。通常,远程代理需要处理网络通信、序列化和反序列化等细节。

1.c) 静态代理步骤总结

通过前四个案例,我们也大致了解了静态代理的使用方式,其大致流程如下:

  • 1.创建一个接口,定义 代理类和被代理类 实现共同的接口
  • 2.创建被代理类,实现这个接口,并且在其中定义实现方法
  • 3.创建代理类,也要实现这个接口,同时在其中定义一个被代理类的对象作为成员变量
  • 4.在代理类中实现接口中的方法,方法中调用 被代理类 中的对应方法
  • 5.通过创建代理对象,并调用其方法,方法增强

这样,被代理类的方法就会被代理类所覆盖,实现了对被代理类的增强或修改

2) 动态代理

静态代理需要手动编写代理类代理类与被代理类实现相同的接口或继承相同的父类 ,对被代理对象进行包装。在程序运行前,代理类的代码就已经生成 ,并在程序运行时调用。静态代理的优点是简单易懂,缺点是需要手动编写代理类,代码复杂度较高,且不易扩展。

动态代理是在程序运行时动态生成代理类 ,无需手动编写代理类,大大降低了代码的复杂度。动态代理一般使用 Java 提供的反射机制实现,可以对任意实现了接口的类进行代理。动态代理的优点是灵活性高,可以根据需要动态生成代理类,缺点是性能相对较低,由于使用反射机制,在运行时会产生额外的开销。

2.a) 基于 JDK 的动态代理实现步骤

使用缓存代理的例子

java 复制代码
public interface DataQuery {
    String query(String queryKey);
}
java 复制代码
public class DatabaseDataQuery implements DataQuery {
    @Override
    public String query(String queryKey) {
        // 使用数据源从数据库查询数据很慢
        return "result";
    }
}

创建一个代理类,实现 InvocationHandler 接口,实现invoke()方法,并在其中定义一个被代理类的对象作为属性。

java 复制代码
public class CacheInvocationHandler implements InvocationHandler {

    private Map<String, String> cache = new HashMap<>(256);

    private DatabaseDataQuery databaseDataQuery;

    public CacheInvocationHandler() {
        this.databaseDataQuery = new DatabaseDataQuery();
    }

    public CacheInvocationHandler(DatabaseDataQuery databaseDataQuery) {
        this.databaseDataQuery = databaseDataQuery;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1.判断是哪一个方法 (只对query方法做缓存)
        String result;
        if ("query".equals(method.getName())) {
            // 2.查缓存
            // 2.1.命中直接返回
            result = cache.get(args[0].toString());
            if (result != null) {
                System.out.println("从缓存拿数据");
                return result;
            }

            // 2.2.未命中,查询数据库 (需要代理实例)
            result = (String) method.invoke(databaseDataQuery, args);

            // 3.查询到了,进行缓存
            cache.put(args[0].toString(), result);
            return result;
        }

        // 当其他的方法被调用,不希望被干预,直接调用原生的方法
        return method.invoke(databaseDataQuery, args);
    }
}

主要业务逻辑 (测试代码)

java 复制代码
@Test
void testJdkDynamicProxy() {
    // jdk提供的代理实现,主要是使用Proxy类来实现
    // 参数1 classLoader:被代理类的类加载器
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    // 参数2 代理类需要实现的接口数组
    Class[] interfaces = new Class[]{DataQuery.class};
    // 参数3 InvocationHandler
    InvocationHandler invocationHandler = new CacheInvocationHandler();

    DataQuery dataQuery = (DataQuery) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);

    // 调用query方法时,实际上是调用了invoke()方法
    String result = dataQuery.query("key1");
    System.out.println(result);
    result = dataQuery.query("key1");
    System.out.println(result);
    result = dataQuery.query("key2");
    System.out.println(result);

    System.out.println("-----------------");
    result = dataQuery.queryAll();
    System.out.println(result);
}

2.b) 基于 CGLIB 的动态代理实现步骤

基于 CGLIB 的动态代理需要使用 net.sf.cglib.proxy.Enhancer 类和 net.sf.cglib.proxy.MethodInterceptor 接口。

1.创建一个被代理类,定义需要被代理的方法 (以DatabaseDataQuery为例)

java 复制代码
public class DatabaseDataQuery {
    public String query(String queryKey) {
        // 使用数据源从数据库查询数据很慢
        System.out.println("正在从数据库中查询数据");
        return "result";
    }

    public String queryAll() {
        System.out.println("正在从数据库中查询数据");
        return "query All result";
    }
}

2.创建一个方法拦截器类,实现 MethodInterceptor 接口,并在其中定义一个被代理类的对象作为属性。

  • intercept 方法中,我们可以对被代理对象的方法进行增强
java 复制代码
public class CacheMethodInterceptor implements MethodInterceptor {
    private Map<String, String> cache = new HashMap<>(256);

    private DatabaseDataQuery databaseDataQuery;

    public CacheMethodInterceptor() {
        this.databaseDataQuery = new DatabaseDataQuery();
    }

    public CacheMethodInterceptor(DatabaseDataQuery databaseDataQuery) {
        this.databaseDataQuery = databaseDataQuery;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 1.判断是哪一个方法
        String result;
        if ("query".equals(method.getName())) {
            // 2.查询缓存,命中则直接返回
            result = cache.get(args[0].toString());
            if (result != null) {
                System.out.println("从缓存中提取数据");
                return result;
            }

            // 3.未命中,查询数据库
            result = (String) method.invoke(databaseDataQuery, args);

            // 4.缓存到缓存中
            cache.put(args[0].toString(), result);
            return result;
        }

        return method.invoke(databaseDataQuery, args);
    }
}

3.在使用代理类时,创建被代理类的对象和代理类的对象,并使用 Enhancer.create 方法生成代理对象。

java 复制代码
@Test
void testCgkibDynamicProxy() {
    // cglib通过enhancer
    Enhancer enhancer = new Enhancer();
    // 1.设置父类
    enhancer.setSuperclass(DatabaseDataQuery.class);
    // 2.设置一个方法拦截器,用来拦截方法
    enhancer.setCallback(new CacheMethodInterceptor());
    // 3.创建代理类
    DatabaseDataQuery databaseDataQuery = (DatabaseDataQuery) enhancer.create();

    String value = databaseDataQuery.query("key1");
    System.out.println(value);
    value = databaseDataQuery.query("key1");
    System.out.println(value);
    value = databaseDataQuery.query("key2");
    System.out.println(value);
}

2.c) Spring中aop的使用步骤

在 Spring 中,AOP(面向切面编程)提供了一种有效的方式来对程序中的多个模块进行横切关注点的处理,例如日志、事务、缓存、安全等。使用 Spring AOP,可以在程序运行时动态地将代码织入到目标对象中,从而实现对目标对象的增强。

Spring AOP 的使用步骤如下:

1.引入 AOP 相关依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.3.9.RELEASE</version>
</dependency>

2.在Main中开启自动代理@EnableAspectJAutoProxy

java 复制代码
@SpringBootApplication
@EnableAspectJAutoProxy
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

3.定义接口和实现类,并将具体实现注入容器 (以DatabaseDataQuery为例)

java 复制代码
// 接口
public interface DataQuery {
    String query(String queryKey);
}

// 实现类
@Component
public class DatabaseDataQuery implements DataQuery {
    @Override
    public String query(String queryKey) {
        // 使用数据源从数据库查询数据很慢
        System.out.println("正在从数据库中查询数据");
        return "result";
    }
}

4.定义切面类,对方法做增强

  • @Pointcut() 对某包下的某个类的某个方法做增强:.. 代表任意方法
  • @Around() 定义增强
java 复制代码
@Component
@Aspect
public class CacheAspectj {

    private static Map<String,String> cache = new ConcurrentHashMap<>(256);

    @Pointcut("execution(* com.dcy.structural.proxy.dynamicProxy.aop.impl.DatabaseDataQuery.query(..))")
    public void pointcut() {}

    @Around("pointcut()")
    public String around(ProceedingJoinPoint joinPoint) {

        // 1.查询缓存
        Object[] args = joinPoint.getArgs();
        String key = args[0].toString();

        // 1.1.命中则返回
        String result = cache.get(key);
        if (result != null) {
            System.out.println("数据从缓存中提取");
            return result;
        }

        // 2.未命中,查询数据库,实际上是调用被代理bean的方法
        try {
            result = joinPoint.proceed().toString();
            // 如果查询有结果,进行缓存
            cache.put(key, result);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

        return result;
    }
}

5.测试用例

java 复制代码
@SpringBootTest
public class AopTest {

    @Resource
    private DataQuery dataQuery;

    @Test
    void testSpringAop() {
        String result = dataQuery.query("key1");
        System.out.println(result);
        result = dataQuery.query("key1");
        System.out.println(result);
        result = dataQuery.query("key2");
        System.out.println(result);
    }

}
相关推荐
reddingtons3 小时前
【游戏宣发】PS “生成式扩展”流,30秒无损适配全渠道KV
游戏·设计模式·新媒体运营·prompt·aigc·教育电商·游戏美术
李慕婉学姐4 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆5 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin6 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20056 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉6 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国6 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882486 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈7 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_997 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc