디자인 패턴_구조 패턴-《에이전트 패턴》

디자인 패턴_구조 패턴-《에이전트 패턴》

다크호스 프로그래머의 자바 디자인 패턴에 대한 자세한 설명, 자바 디자인 패턴 23개(다이어그램 + 프레임워크 소스코드 분석 + 실전)로 노트 정리

구조 패턴은 특정 레이아웃에서 클래스 또는 개체를 더 큰 구조로 구성하는 방법을 설명합니다. 클래스 구조 패턴과 객체 구조 패턴으로 나뉘는데 전자는 상속 메커니즘을 사용하여 인터페이스와 클래스를 구성하고 후자는 결합 또는 집계를 사용하여 객체를 결합합니다.

조합 또는 집계 관계는 상속 관계보다 결합도가 낮고 "복합 재사용 원칙"을 충족하므로 개체 구조 모델이 클래스 구조 모델보다 더 큰 유연성을 갖습니다.

구조 패턴은 다음 7가지 유형으로 나뉩니다.

  • 프록시 모드
  • 어댑터 패턴
  • 데코레이터 패턴
  • 브리지 모드
  • 외모 모드
  • 조합 모드
  • 플라이급 모드

개요

프록시 패턴 : 어떤 이유로 객체에 대한 액세스를 제어하기 위해 객체에 프록시를 제공해야 합니다. 이때 접근 객체는 적합하지 않거나 대상 객체를 직접 참조할 수 없으며 프록시 객체는 접근 객체와 대상 객체 사이의 연결 고리 역할을 한다 中介.

Java에서 프록시는 프록시 클래스의 생성 시점에 따라 정적 프록시와 동적 프록시로 나뉩니다.

  • 정적 프록시 프록시 클래스는 컴파일 시간에 생성됩니다.
  • 동적 프록시 프록시 클래스는 Java 런타임에 동적으로 생성됩니다. 동적 프록시에는 다음이 있습니다.
    • JDK 프록시
    • CGLib 프록시

구조

프록시 모드는 세 가지 역할로 나뉩니다.

  • 추상 주체(Subject) 클래스: 인터페이스나 추상 클래스를 통해 실제 주체와 프록시 객체가 구현한 비즈니스 메서드를 선언합니다.
  • 실제 주제(Real Subject) 클래스: 추상 주제에서 특정 업무를 구현하고 프록시 객체로 표현되는 실제 객체이며 참조되는 최종 객체입니다.
  • 프록시(Proxy) 클래스: 실제 테마에 대한 참조를 포함하는 실제 테마와 동일한 인터페이스를 제공하여 실제 테마의 기능에 액세스, 제어 또는 확장할 수 있습니다.

정적 프록시

사례를 통해 정적 대리를 살펴보자.

[예시] 기차역에서 티켓 판매

기차표를 사고 싶다면 기차표를 사기 위해 기차역에 가서 기차를 타고 기차역까지 가서 줄을 서서 일련의 작업을 기다려야 하는데, 이는 분명히 더 번거로운 일입니다. 그리고 기차역에는 여러 곳에 매표소가 있기 때문에 매표소에 가서 표를 사는 것이 훨씬 더 편리합니다. 이 예제는 실제로 전형적인 에이전트 모델이며 기차역은 대상 개체이고 판매 대리점은 에이전트 개체입니다. 클래스 다이어그램은 다음과 같습니다.

아래와 같이 코드 쇼:

  • 추상 테마 클래스 - 티켓 인터페이스

    public interface SellTickets {
          
          
        void sell();
    }
    
  • 실제 테마 클래스 - 기차역: 기차역에는 티켓 판매 기능이 있으므로 SellTickets 인터페이스를 구현해야 합니다.

    public class TrainStation implements SellTickets {
          
          
    
        public void sell() {
          
          
            System.out.println("火车站卖票");
        }
    }
    
  • 에이전트 클래스 - 판매 시점

    public class ProxyPoint implements SellTickets {
          
          
    
        // 声明火车站类对象
        private TrainStation station = new TrainStation();
    
        public void sell() {
          
          
            System.out.println("代售点收取一些服务费用");
            station.sell();
        }
    }
    
  • 테스트 클래스

    public class Client {
          
          
        public static void main(String[] args) {
          
          
            // 创建代理对象
            ProxyPoint pp = new ProxyPoint();
            // 调用代理对象的方法进行买票 最终调用的还是火车站的方法
            pp.sell();
        }
    }
    

    산출

    代售点收取一些服务费用
    火车站卖票
    

위의 코드에서 테스트 클래스가 ProxyPoint 클래스 개체에 직접 액세스한다는 것을 알 수 있습니다. 즉, ProxyPoint는 액세스 개체와 대상 개체 사이에서 중개자 역할을 합니다. 동시에 매도 방식이 강화되었습니다(대리점에서 일부 서비스 수수료 부과).

JDK 동적 프록시

다음으로 위의 경우를 구현하기 위해 동적프록시를 사용하는데 먼저 JDK에서 제공하는 동적프록시에 대해 알아보겠습니다. Java는 Proxy라는 동적인 프록시 클래스를 제공하는데, Proxy는 위에서 언급한 프록시 객체의 클래스가 아니라 프록시 객체를 얻기 위해 프록시 객체를 생성하는 정적 메서드(newProxyInstance 메서드)를 제공합니다.

아래와 같이 코드 쇼:

  • 티켓 인터페이스

    public interface SellTickets {
          
          
        void sell();
    }
    
  • 기차역: 기차역에는 티켓 판매 기능이 있으므로 SellTickets 인터페이스를 구현해야 합니다.

    public class TrainStation implements SellTickets {
          
          
    
        public void sell() {
          
          
            System.out.println("火车站卖票");
        }
    }
    
  • 프록시 개체를 만드는 데 사용되는 프록시 팩토리

    public class ProxyFactory {
          
          
    
        private TrainStation station = new TrainStation();
    
        // 获取代理对象的方法
        public SellTickets getProxyObject() {
          
          
            // 使用Proxy获取代理对象
            /*
             * newProxyInstance()方法参数说明:
             * ClassLoader loader:类加载器,用于加载代理类,使用真实对象的类加载器即可
             * Class<?>[] interfaces:真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
             * InvocationHandler h:代理对象的调用处理程序
             */
            SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(
                	station.getClass().getClassLoader(),
                    station.getClass().getInterfaces(),
                    new InvocationHandler() {
          
          
                        /*
                         * InvocationHandler中invoke方法参数说明:
                         * Object proxy:代理对象,和proxyObject对象是同一个对象,在invoke方法中基本不用
                         * Method method:对应于在代理对象上调用的接口方法的 Method 实例
                         * Object[] args:代理对象调用接口方法时传递的实际参数
                         * 返回值:就是方法的返回值
                         */
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          
          
                            System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
                            // 执行真实对象
                            Object result = method.invoke(station, args);
                            return result;
                        }
                    });
            return sellTickets;
        }
    }
    
  • 테스트 클래스

    public class Client {
          
          
        public static void main(String[] args) {
          
          
            // 获取代理对象
            // 1.创建代理工厂对象
            ProxyFactory factory = new ProxyFactory();
            // 2.使用factory对象的方法获取代理对象
            SellTickets proxyObject = factory.getProxyObject();
            // 3.调用卖票的方法
            proxyObject.sell();
        }
    }
    

    산출

    代理点收取一定的服务费用(JDK动态代理方式)
    火车站卖票
    

동적 프록시를 사용하여 다음 질문에 대해 생각합니다.

  • ProxyFactory는 프록시 클래스입니까?

    ProxyFactory는 프록시 모드에서 언급한 프록시 클래스가 아니라 代理类是程序在运行过程中动态的在内存中生成的类. Alibaba의 오픈 소스 Java 진단 도구(Arthas [Alsace])를 통해 프록시 클래스의 구조 보기:

    package com.sun.proxy;
    
    import com.itheima.proxy.dynamic.jdk.SellTickets;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    
    // jdk动态生成的类,实现了SellTickets接口
    public final class $Proxy0 extends Proxy implements SellTickets {
          
          
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
    
        public $Proxy0(InvocationHandler invocationHandler) {
          
          
            // 构造方法传入的InvocationHandler(也就是我们自己new的InvocationHandler)赋值给父类的InvocationHandler属性
            super(invocationHandler);
        }
    
        static {
          
          
            try {
          
          
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
                // 只有这个m3是我们需要注意的
                // 通过全类名加载了接口 再去拿到接口中的同名方法 将这个Method对象赋值给m3
                m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
                m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
                return;
            }
            catch (NoSuchMethodException noSuchMethodException) {
          
          
                throw new NoSuchMethodError(noSuchMethodException.getMessage());
            }
            catch (ClassNotFoundException classNotFoundException) {
          
          
                throw new NoClassDefFoundError(classNotFoundException.getMessage());
            }
        }
    
        // 省略了equals、toString、hashCode方法...
    
        // 卖票方法
        public final void sell() {
          
          
            try {
          
          
                // 调用h.invoke 其实就是调用我们自己实现的InvocationHandler中的invoke方法
                /*
                 * this:当前对象(代理类$Proxy0)
                 * m3:要执行的方法
                 * null:方法没有参数,所以为null
                 */
                this.h.invoke(this, m3, null);
                return;
            }
            catch (Error | RuntimeException throwable) {
          
          
                throw throwable;
            }
            catch (Throwable throwable) {
          
          
                throw new UndeclaredThrowableException(throwable);
            }
        }
    }
    
    // 父类Proxy
    public class Proxy implements java.io.Serializable {
          
          
        
        // InvocationHandler属性 声明为h
        protected InvocationHandler h;
    }
    

    위의 클래스에서 다음 정보를 볼 수 있습니다.

    • 프록시 클래스($Proxy0)는 SellTickets 인터페이스를 구현합니다. 이것은 또한 실제 클래스와 프록시 클래스가 동일한 인터페이스를 구현한다고 이전에 말한 것을 확인합니다.
    • 프록시 클래스($Proxy0)는 우리가 제공한 익명 내부 클래스 개체 InvocationHandler를 부모 클래스에 전달합니다.
  • 동적 프록시의 실행 프로세스는 무엇입니까?

    다음은 추출된 키 코드입니다.

    // 程序运行过程中动态生成的代理类
    public final class $Proxy0 extends Proxy implements SellTickets {
          
          
        private static Method m3;
    
        public $Proxy0(InvocationHandler invocationHandler) {
          
          
            super(invocationHandler);
        }
    
        static {
          
          
            m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
        }
    
        public final void sell() {
          
          
            this.h.invoke(this, m3, null);
        }
    }
    
    // Java提供的动态代理相关类
    public class Proxy implements java.io.Serializable {
          
          
    	protected InvocationHandler h;
    	 
    	protected Proxy(InvocationHandler h) {
          
          
            this.h = h;
        }
    }
    
    // 代理工厂类
    public class ProxyFactory {
          
          
    
        private TrainStation station = new TrainStation();
    
        public SellTickets getProxyObject() {
          
          
            SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
                    station.getClass().getInterfaces(),
                    new InvocationHandler() {
          
          
                        
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          
          
    
                            System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
                            Object result = method.invoke(station, args);
                            return result;
                        }
                    });
            return sellTickets;
        }
    }
    
    // 测试访问类
    public class Client {
          
          
        public static void main(String[] args) {
          
          
            // 获取代理对象
            ProxyFactory factory = new ProxyFactory();
            SellTickets proxyObject = factory.getProxyObject();
            proxyObject.sell();
        }
    }
    

실행 프로세스는 다음과 같습니다.

  1. 테스트 클래스의 프록시 개체를 통해 sell() 메서드 호출
  2. 다형성의 특성에 따라 프록시 클래스($Proxy0)의 sell() 메서드가 실행됩니다.
  3. 프록시 클래스($Proxy0)의 sell() 메서드는 InvocationHandler 인터페이스의 하위 구현 클래스 객체의 invoke 메서드를 호출합니다.
  4. invoke 메소드는 리플렉션을 통해 실제 객체(TrainStation)의 클래스에서 sell() 메소드를 실행합니다.

CGLIB 동적 프록시

위와 동일한 경우 CGLIB 프록시 구현을 다시 사용합니다.

SellTickets 인터페이스가 정의되지 않은 경우 TrainStation(기차역 클래스)만 정의됩니다. 분명히 JDK 프록시는 사용할 수 없습니다. JDK 동적 프록시에는 인터페이스 정의 및 인터페이스 프록시가 필요하기 때문입니다.

CGLIB는 강력한 고성능 코드 생성 패키지입니다. 인터페이스를 구현하지 않는 클래스에 대한 프록시를 제공하며 JDK의 동적 프록시를 보완합니다.

CGLIB는 타사에서 제공하는 패키지이므로 jar 패키지의 좌표를 입력해야 합니다.

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

아래와 같이 코드 쇼:

  • 기차역

    public class TrainStation {
          
          
    
        public void sell() {
          
          
            System.out.println("火车站卖票");
        }
    }
    
  • 프록시 개체를 가져오는 데 사용되는 프록시 팩토리

    public class ProxyFactory implements MethodInterceptor {
          
          
    
        // 声明火车站对象
        private TrainStation target = new TrainStation();
    
        public TrainStation getProxyObject() {
          
          
            // 创建Enhancer对象,类似于JDK动态代理的Proxy类。下一步就是设置几个参数
            Enhancer enhancer = new Enhancer();
            // 设置父类的字节码对象(CGLIB的代理类属于目标类的子类)所以这里直接用TrainStation的字节码对象
            enhancer.setSuperclass(target.getClass());
            // 设置回调函数,参数是MethodInterceptor子实现类的对象,使代理工厂实现这个接口,
            // 所以这里传入this 该接口的子实现类对象即可。
            enhancer.setCallback(this);
            // 创建代理对象
            TrainStation obj = (TrainStation) enhancer.create();
            return obj;
        }
    
        /*
         * intercept方法参数说明:
         * o:代理对象
         * method:真实对象中的方法的Method实例
         * args:实际参数
         * methodProxy:代理对象中的方法的method实例
         */
        // 通过代理对象调用需要执行的方法,就会调用intercept方法,这个就是上面的回调函数,设置的是方法所属类的对象this。
        public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
          
          
            System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)");
            // 调用目标对象的方法
            //Object obj = method.invoke(station, objects);
            TrainStation result = (TrainStation) methodProxy.invokeSuper(o, args);
            return result;
        }
    }
    
  • 테스트 클래스

    public class Client {
          
          
        public static void main(String[] args) {
          
          
            // 创建代理工厂对象
            ProxyFactory factory = new ProxyFactory();
            // 获取代理对象
            TrainStation proxyObject = factory.getProxyObject();
    		// 调用代理对象的方法卖票
            proxyObject.sell();
        }
    }
    

    산출

    代理点收取一些的服务费用(CGLIB动态代理方式)
    火车站卖票
    

    MethodProxy#invokeSuper를 사용하는 것과 cglib 동적 프록시 인터셉터에서 호출하는 것의 차이점

세 가지 에이전트 비교

  • jdk 프록시 및 CGLIB 프록시
    • CGLib를 사용하여 동적 프록시를 구현하십시오.CGLib의 맨 아래 계층은 ASM 바이트 코드 생성 프레임워크와 바이트 코드 기술을 채택하여 프록시 클래스를 생성하며 이는 JDK1.6 이전의 Java 리플렉션을 사용하는 것보다 더 효율적입니다. CGLib의 원칙은 프록시된 클래스의 하위 클래스를 동적으로 생성하는 것이기 때문에 CGLib는 최종으로 선언된 클래스 또는 메서드를 프록시할 수 없다는 점에 유의해야 합니다.
    • JDK1.6, JDK1.7, JDK1.8이 점차적으로 JDK 동적 프록시를 최적화한 후 JDK 프록시의 효율성은 호출 수가 적을 때 CGLib 프록시보다 높습니다. made, JDK1.6과 JDK1.7은 CGLib 프록시보다 약간 덜 효율적이지만 JDK1.8의 경우 JDK 프록시가 CGLib 프록시보다 효율적입니다. 따라서 인터페이스가 있으면 JDK 동적 프록시를 사용하고 인터페이스가 없으면 CGLIB 프록시를 사용하십시오.
  • 동적 프록시 및 정적 프록시
    • 정적 프록시와 비교할 때 동적 프록시의 가장 큰 장점은 인터페이스에 선언된 모든 메서드가 호출 핸들러(InvocationHandler.invoke)의 중앙 집중식 메서드로 전달된다는 것입니다. 이렇게 하면 인터페이스 메서드가 많을 때 정적 프록시처럼 각 메서드를 전달할 필요 없이 유연하게 처리할 수 있습니다.
    • 인터페이스가 메서드를 추가하면 정적 프록시 모드에서 이 메서드를 구현해야 하는 모든 구현 클래스 외에도 모든 프록시 클래스도 이 메서드를 구현해야 합니다. 코드 유지 관리의 복잡성이 증가했습니다. 이 문제는 동적 프록시에서는 발생하지 않습니다.

장점과 단점

이점

  • 프록시 모드는 클라이언트와 대상 개체 사이에서 매개 역할을 하며 대상 개체를 보호합니다.
  • 프록시 개체는 대상 개체의 기능을 확장할 수 있습니다(기능 향상).
  • 프록시 모드는 대상 개체에서 클라이언트를 분리하여 시스템의 결합을 어느 정도 줄일 수 있습니다.

결점

  • 시스템 복잡성 증가

사용되는 장면

  • 원격 프록시
    • 로컬 서비스는 네트워크를 통해 원격 서비스를 요청합니다. 로컬에서 원격으로 통신하려면 네트워크 통신을 구현하고 가능한 예외를 처리해야 합니다. 좋은 코드 설계와 유지보수성을 위해 네트워크 통신 부분을 숨기고 로컬 서비스에 대한 인터페이스만 노출하여 통신 부분의 세부 사항에 크게 신경을 쓰지 않고도 원격 서비스에서 제공하는 기능에 액세스할 수 있습니다.
  • 방화벽 프록시
    • 프록시 기능을 사용하도록 브라우저를 구성하면 방화벽이 브라우저의 요청을 인터넷으로 전달하고 인터넷이 응답을 반환하면 프록시 서버가 이를 브라우저로 전달합니다.
  • 에이전트 보호 또는 액세스
    • 개체에 대한 액세스를 제어하고 원하는 경우 다른 사용자에게 다른 수준의 액세스를 제공합니다.

추천

출처blog.csdn.net/weixin_53407527/article/details/128627869