차이점: 정적 에이전트는 프로그래머에 의해 생성되거나 도구가 에이전트 클래스의 소스 코드를 생성한 다음 프로그램이 실행되기 전에 이미 존재하는 에이전트 클래스의 바이트코드 파일과 에이전트 클래스 간의 관계를 컴파일합니다. 대리자 클래스는 실행 전에 결정됩니다. 동적 프록시 클래스의 소스 코드는 프로그램 실행 중 리플렉션과 같은 메커니즘을 기반으로 JVM에 의해 동적으로 생성되므로 프록시 클래스에 대한 바이트코드 파일이 없습니다.
관련 권장 사항: "프로그래밍 비디오 코스"
1. 프록시 개념
개체에 대한 프록시를 제공하여 이 개체에 대한 액세스를 제어합니다. 프록시 클래스와 델리게이트 클래스는 공통의 부모 클래스 또는 부모 인터페이스를 가지므로 델리게이트 클래스 객체가 사용되는 곳이면 어디든 프록시 객체를 사용할 수 있습니다. 프록시 클래스는 요청 전처리, 필터링, 처리를 위해 대리자 클래스에 요청 할당, 대리자 클래스가 요청을 완료한 후 후속 처리를 담당합니다. 관련 권장 사항: "Java Video Tutorial"
그림 1: 프록시 모드
그림에서 볼 수 있듯이 프록시 인터페이스(Subject), 프록시 클래스(ProxySubject), 위임자 클래스( RealSubject)는 "PIN" 구조를 형성합니다.
에이전트 클래스의 생성 시간에 따라 에이전트는 정적 에이전트와 동적 에이전트의 두 가지 유형으로 나눌 수 있습니다.
다음은 정적 에이전트와 동적 에이전트를 설명하기 위한 시뮬레이션 요구 사항입니다. 대리자 클래스는 장시간 작업을 처리해야 하고, 클라이언트 클래스는 작업 실행에 소요되는 시간을 인쇄해야 합니다. 이 문제를 해결하려면 작업 실행 전 시간과 작업 실행 후 시간의 차이를 기록해야 합니다.
2. 정적 프록시
는 프로그래머가 생성하거나 도구가 프록시 클래스의 소스 코드를 생성한 후 프록시 클래스를 컴파일합니다. 소위 정적이란 프로그램이 실행되기 전에 프록시 클래스의 바이트코드 파일이 이미 존재한다는 것을 의미합니다. 프록시 클래스와 대리자 클래스 간의 관계는 실행 전에 결정됩니다.
목록 1: 프록시 인터페이스
/** * 代理接口。处理给定名字的任务。 */ public interface Subject { /** * 执行给定名字的任务。 * @param taskName 任务名 */ public void dealTask(String taskName); }
목록 2: 위임 클래스, 특히 비즈니스 처리.
/** * 真正执行任务的类,实现了代理接口。 */ public class RealSubject implements Subject { /** * 执行给定名字的任务。这里打印出任务名,并休眠500ms模拟任务执行了很长时间 * @param taskName */ @Override public void dealTask(String taskName) { System.out.println("正在执行任务:"+taskName); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }
목록 3: 정적 프록시 클래스
/** * 代理类,实现了代理接口。 */ public class ProxySubject implements Subject { //代理类持有一个委托类的对象引用 private Subject delegate; public ProxySubject(Subject delegate) { this.delegate = delegate; } /** * 将请求分派给委托类执行,记录任务执行前后的时间,时间差即为任务的处理时间 * * @param taskName */ @Override public void dealTask(String taskName) { long stime = System.currentTimeMillis(); //将请求分派给委托类处理 delegate.dealTask(taskName); long ftime = System.currentTimeMillis(); System.out.println("执行任务耗时"+(ftime - stime)+"毫秒"); } }
목록 4: 정적 프록시 클래스 팩토리 생성
public class SubjectStaticFactory { //客户类调用此工厂方法获得代理对象。 //对客户类来说,其并不知道返回的是代理类对象还是委托类对象。 public static Subject getInstance(){ return new ProxySubject(new RealSubject()); } }
清单5:客户类
public class Client1 { public static void main(String[] args) { Subject proxy = SubjectStaticFactory.getInstance(); proxy.dealTask("DBQueryTask"); } }
静态代理类优缺点
优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是代理的共有优点。
缺点:
1)代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。
2)如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
三、动态代理
动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。
1、先看看与动态代理紧密关联的Java API。
1)java.lang.reflect.Proxy
这是 Java 动态代理机制生成的所有动态代理类的父类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。
清单6:Proxy类的静态方法
// 方法 1: 该方法用于获取指定代理对象所关联的调用处理器 static InvocationHandler getInvocationHandler(Object proxy) // 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象 static Class getProxyClass(ClassLoader loader, Class[] interfaces) // 方法 3:该方法用于判断指定类对象是否是一个动态代理类 static boolean isProxyClass(Class cl) // 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例 static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
2)java.lang.reflect.InvocationHandler
这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。每次生成动态代理类对象时都要指定一个对应的调用处理器对象。
清单7:InvocationHandler的核心方法
// 该方法负责集中处理动态代理类上的所有方法调用。第一个参数既是代理类实例,第二个参数是被调用的方法对象 // 第三个方法是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上反射执行 Object invoke(Object proxy, Method method, Object[] args)
3)java.lang.ClassLoader
这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。
每次生成动态代理类对象时都需要指定一个类装载器对象
2、动态代理实现步骤
具体步骤是:
a. 实现InvocationHandler接口创建自己的调用处理器
b. 给Proxy类提供ClassLoader和代理接口类型数组创建动态代理类
c. 以调用处理器类型为参数,利用反射机制得到动态代理类的构造函数
d. 以调用处理器对象为参数,利用动态代理类的构造函数创建动态代理类对象
清单8:分步骤实现动态代理
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发 // 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用 InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象 Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); // 通过反射从生成的类对象获得构造函数对象 Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); // 通过构造函数对象创建动态代理类实例 Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });
Proxy类的静态方法newProxyInstance对上面具体步骤的后三步做了封装,简化了动态代理对象的获取过程。
清单9:简化后的动态代理实现
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发 InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 直接创建动态代理类实例 Interface proxy = (Interface)Proxy.newProxyInstance( classLoader, new Class[] { Interface.class }, handler );
3、动态代理实现示例
清单10:创建自己的调用处理器
/** * 动态代理类对应的调用处理程序类 */ public class SubjectInvocationHandler implements InvocationHandler { //代理类持有一个委托类的对象引用 private Object delegate; public SubjectInvocationHandler(Object delegate) { this.delegate = delegate; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long stime = System.currentTimeMillis(); //利用反射机制将请求分派给委托类处理。Method的invoke返回Object对象作为方法执行结果。 //因为示例程序没有返回值,所以这里忽略了返回值处理 method.invoke(delegate, args); long ftime = System.currentTimeMillis(); System.out.println("执行任务耗时"+(ftime - stime)+"毫秒"); return null; } }
清单11:生成动态代理对象的工厂,工厂方法列出了如何生成动态代理类对象的步骤。
/** * 生成动态代理对象的工厂. */ public class DynProxyFactory { //客户类调用此工厂方法获得代理对象。 //对客户类来说,其并不知道返回的是代理类对象还是委托类对象。 public static Subject getInstance(){ Subject delegate = new RealSubject(); InvocationHandler handler = new SubjectInvocationHandler(delegate); Subject proxy = null; proxy = (Subject)Proxy.newProxyInstance( delegate.getClass().getClassLoader(), delegate.getClass().getInterfaces(), handler); return proxy; } }
清单12:动态代理客户类
public class Client { public static void main(String[] args) { Subject proxy = DynProxyFactory.getInstance(); proxy.dealTask("DBQueryTask"); } }
4、动态代理机制特点
首先是动态生成的代理类本身的一些特点。1)包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.ibm.developerworks 包中的某非 public 接口 A,那么新生成的代理类所在的包就是 com.ibm.developerworks),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;2)类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;3)类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。4)类继承关系:该类的继承关系如图:
图2:动态代理类的继承关系
그림에서 볼 수 있듯이 Proxy 클래스는 상위 클래스입니다. 이 규칙은 Proxy에서 생성된 모든 동적 프록시 클래스에 적용됩니다. 그리고 이 클래스는 프록시하는 인터페이스 세트도 구현합니다. 이것이 프록시하는 인터페이스에 안전하게 유형 변환할 수 있는 근본적인 이유입니다.
다음으로 프록시 클래스 인스턴스의 몇 가지 특징을 살펴보겠습니다. 각 인스턴스는 호출 핸들러 객체와 연관되어 있습니다. Proxy에서 제공하는 정적 메소드 getInvocationHandler를 통해 프록시 클래스 인스턴스의 호출 핸들러 객체를 얻을 수 있습니다. 프록시의 인터페이스에 선언된 메소드가 프록시 클래스 인스턴스에서 호출되면 이러한 메소드는 결국 호출 프로세서의 호출 메소드에 의해 실행됩니다. 또한 루트 클래스 java에 세 가지가 있다는 점은 주목할 가치가 있습니다. 프록시 클래스의 lang.Object 메소드는 실행을 위해 호출 프로세서의 호출 메소드로 전달되며, 가능한 이유는 다음과 같습니다. 첫째, 이러한 메소드는 공개 유형이 아니기 때문입니다. 둘째, 이러한 메서드는 종종 클래스의 특정 특징적인 특성을 나타내고 어느 정도 구별이 있기 때문에 프록시 클래스와 대리자 클래스의 외부 일관성을 보장하기 위해 이러한 메서드는 프록시 클래스에 의해 재정의될 수 있습니다. 실행을 위해 세 가지 메서드도 대리자 클래스에 할당되어야 합니다. 프록시의 인터페이스 세트에 반복적으로 선언된 메소드가 있고 해당 메소드가 호출되면 프록시 클래스는 프록시 클래스 인스턴스가 해당 인터페이스를 사용하는지 여부에 관계없이 항상 맨 앞 인터페이스에서 메소드 객체를 가져와 호출 핸들러로 전달합니다. .(또는 이 인터페이스에서 상속된 하위 인터페이스)은 현재 참조된 유형을 프록시 클래스 내에서 구별할 수 없기 때문에 외부에서 참조됩니다.
다음으로 프록시되는 인터페이스 그룹의 특징에 대해 알아 보겠습니다. 우선, 동적 프록시 클래스 코드를 생성할 때 컴파일 오류를 방지하려면 중복된 인터페이스가 없도록 주의하세요. 둘째, 이러한 인터페이스는 클래스 로더에 표시되어야 합니다. 그렇지 않으면 클래스 로더가 이를 연결할 수 없어 클래스 정의가 실패하게 됩니다. 셋째, 프록시해야 하는 모든 비공개 인터페이스는 동일한 패키지에 있어야 합니다. 그렇지 않으면 프록시 클래스 생성도 실패합니다. 마지막으로 인터페이스 수는 JVM에서 설정한 제한인 65535를 초과할 수 없습니다.
마지막으로 예외 처리의 특징을 살펴보겠습니다. 호출 프로세서 인터페이스에 의해 선언된 메서드에서 이론적으로 모든 예외가 Throwable 인터페이스에서 상속되기 때문에 모든 유형의 예외를 throw할 수 있다는 것을 알 수 있지만 이것이 사실일까요? 대답은 '아니요'입니다. 상속 원칙을 준수해야 하기 때문입니다. 하위 클래스가 상위 클래스의 메서드를 재정의하거나 상위 인터페이스를 구현할 때 발생하는 예외는 원래 메서드에서 지원하는 예외 목록 내에 있어야 합니다. 따라서 이론적으로는 핸들러 호출이 가능하지만 실제로는 상위 인터페이스의 메소드가 Throwable 예외 발생을 지원하지 않는 한 제한되는 경우가 많습니다. 그러면 인터페이스 메소드 선언에서 지원되지 않는 예외가 호출 메소드에서 발생하면 어떻게 될까요? 걱정하지 마십시오. Java 동적 프록시 클래스는 이미 우리를 위한 솔루션을 설계했습니다. UndeclaredThrowableException 예외가 발생합니다. 이 예외는 RuntimeException 유형이므로 컴파일 오류가 발생하지 않습니다. 예외의 getCause 메소드를 통해 오류 진단을 용이하게 하기 위해 지원되지 않는 원래 예외 객체를 얻을 수도 있습니다.
5. 동적 프록시의 장점과 단점
장점:
정적 프록시와 비교하여 동적 프록시의 가장 큰 장점은 인터페이스에 선언된 모든 메서드가 호출 프로세서(InvocationHandler.invoke)의 중앙 집중식 메서드로 전송된다는 것입니다. 이런 방식으로 인터페이스 메소드가 많을 때 정적 프록시처럼 각 메소드를 전송하지 않고도 유연하게 처리할 수 있습니다. Invoke 메소드 바디에 특정 주변 서비스가 내장되어 있기 때문에(작업 처리 전후의 시간을 기록하고 시간 차이를 계산함) 실제로 주변 서비스를 Spring AOP와 유사하게 구성할 수 있으므로 이 예제에서는 볼 수 없습니다.
고충:
Proxy가 매우 아름답게 디자인된 것은 사실이지만, 아직 조금 아쉬운 부분이 있습니다. 즉, 인터페이스 프록시만 지원한다는 족쇄에서 결코 벗어날 수 없다는 것입니다. 그 디자인은 운명이 정해져 있습니다. 안타깝습니다. 동적으로 생성된 프록시 클래스의 상속 다이어그램을 떠올려 보세요. 이들은 Proxy라는 공통 상위 클래스를 갖게 됩니다. Java의 상속 메커니즘은 이러한 동적 프록시 클래스가 클래스에 대한 동적 프록시를 구현할 수 없도록 되어 있습니다. 그 이유는 다중 상속이 기본적으로 Java에서 작동하지 않기 때문입니다.
사람들이 클래스 프록시의 필요성을 부정하는 데에는 여러 가지 이유가 있지만 클래스 동적 프록시를 지원하는 것이 더 낫다고 믿을 만한 몇 가지 이유도 있습니다. 인터페이스와 클래스 사이의 구분은 처음에는 그다지 명확하지 않습니다. Java에서만 그렇게 자세하게 설명됩니다. 메소드 선언과 정의 여부만 고려하면 둘이 혼합되어 있으며 그 이름은 추상 클래스입니다. 나는 추상 클래스에 대한 동적 프록시를 구현하는 것에도 고유한 가치가 있다고 믿습니다. 또한, 인터페이스를 구현하지 않기 때문에 동적 에이전트와 연결되지 않는 일부 클래스가 기록에 남아 있습니다. 이 모든 것은 작은 후회라고 말하고 싶습니다.
더 많은 관련 글을 읽고 싶으시면 PHP 중국어 홈페이지를 방문해주세요! !
위 내용은 정적 프록시와 동적 프록시의 차이점은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!