使用 代理模式 实现一个“干饭村约会系统服务”的示例,能够通过代理控制对实际对象(比如用户的约会资料)访问、保护隐私、限制不正当操作等。
Person
接口定义用户基本操作的接口。
public interface Person {
String getName();
String getGender();
String getInterests();
int getHotOrNotRating();
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
void setHotOrNotRating(int rating);
}
PersonImpl
类(实际的用户类)这是实际的用户实现,存储用户的资料。
public class PersonImpl implements Person {
private String name;
private String gender;
private String interests;
private int rating;
private int ratingCount = 0;
@Override
public String getName() {
return name;
}
@Override
public String getGender() {
return gender;
}
@Override
public String getInterests() {
return interests;
}
@Override
public int getHotOrNotRating() {
if (ratingCount == 0) return 0;
return rating / ratingCount;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setGender(String gender) {
this.gender = gender;
}
@Override
public void setInterests(String interests) {
this.interests = interests;
}
@Override
public void setHotOrNotRating(int rating) {
this.rating += rating;
ratingCount++;
}
}
OwnerProxy
和 NonOwnerProxy
OwnerProxy
类(自己代理)用户只能修改自己的资料,但不能为自己打分。
public class OwnerProxy implements Person {
private Person person;
public OwnerProxy(Person person) {
this.person = person;
}
@Override
public String getName() {
return person.getName();
}
@Override
public String getGender() {
return person.getGender();
}
@Override
public String getInterests() {
return person.getInterests();
}
@Override
public int getHotOrNotRating() {
return person.getHotOrNotRating();
}
@Override
public void setName(String name) {
person.setName(name);
}
@Override
public void setGender(String gender) {
person.setGender(gender);
}
@Override
public void setInterests(String interests) {
person.setInterests(interests);
}
@Override
public void setHotOrNotRating(int rating) {
throw new UnsupportedOperationException("You cannot rate yourself.");
}
}
NonOwnerProxy
类(其他人代理)其他人可以查看用户的资料并为其打分,但不能修改资料。
public class NonOwnerProxy implements Person {
private Person person;
public NonOwnerProxy(Person person) {
this.person = person;
}
@Override
public String getName() {
return person.getName();
}
@Override
public String getGender() {
return person.getGender();
}
@Override
public String getInterests() {
return person.getInterests();
}
@Override
public int getHotOrNotRating() {
return person.getHotOrNotRating();
}
@Override
public void setName(String name) {
throw new UnsupportedOperationException("You cannot change someone else's name.");
}
@Override
public void setGender(String gender) {
throw new UnsupportedOperationException("You cannot change someone else's gender.");
}
@Override
public void setInterests(String interests) {
throw new UnsupportedOperationException("You cannot change someone else's interests.");
}
@Override
public void setHotOrNotRating(int rating) {
person.setHotOrNotRating(rating);
}
}
PersonProxyFactory
工厂类用于创建不同类型的代理。
public class PersonProxyFactory {
public static Person getOwnerProxy(Person person) {
return new OwnerProxy(person);
}
public static Person getNonOwnerProxy(Person person) {
return new NonOwnerProxy(person);
}
}
展示如何使用代理模式控制用户资料的访问权限。
public class DatingSystemTest {
public static void main(String[] args) {
Person joe = new PersonImpl();
joe.setName("Joe");
joe.setGender("Male");
joe.setInterests("Eating, Cooking");
// 作为自己访问
Person ownerProxy = PersonProxyFactory.getOwnerProxy(joe);
System.out.println("Name: " + ownerProxy.getName());
ownerProxy.setInterests("Gaming, Running"); // 修改成功
try {
ownerProxy.setHotOrNotRating(10); // 无法为自己打分
} catch (Exception e) {
System.out.println("Cannot rate yourself.");
}
// 作为他人访问
Person nonOwnerProxy = PersonProxyFactory.getNonOwnerProxy(joe);
System.out.println("Name: " + nonOwnerProxy.getName());
try {
nonOwnerProxy.setInterests("Hiking"); // 无法修改他人资料
} catch (Exception e) {
System.out.println("Cannot modify interests.");
}
nonOwnerProxy.setHotOrNotRating(8); // 允许为他人打分
System.out.println("Rating: " + nonOwnerProxy.getHotOrNotRating());
}
}
Name: Joe
Cannot rate yourself.
Name: Joe
Cannot modify interests.
Rating: 8
Person
接口:定义了用户的基本操作,如获取和设置姓名、性别、兴趣以及打分等功能。PersonImpl
类:用户的实际实现类,存储用户的基本信息。OwnerProxy
:允许用户修改自己的资料,但不允许为自己打分。NonOwnerProxy
:允许其他人查看用户的资料并为其打分,但不允许修改资料。PersonProxyFactory
:通过该工厂类,可以为用户生成自己代理(OwnerProxy
)或他人代理(NonOwnerProxy
)。DatingSystemTest
:测试代理模式的功能,展示不同代理如何控制对用户资料的访问。代理模式为其他对象提供一个代理或占位符来控制对这个对象的访问。可以将代理模式理解为通过代理类来间接访问目标对象,从而可以增加一些额外的功能,如权限控制、延迟加载、日志记录等。
Subject
接口:定义了目标对象和代理类的通用接口,确保它们具有一致的行为,使得客户端可以通过接口与真实对象或代理对象进行交互。
RealSubject
真实主题类:实际需要访问的对象,代理模式的目标对象,完成实际的操作和功能。
Proxy
代理类:代理对象,控制对真实对象的访问。代理类可以在访问真实对象之前或之后增加额外的行为,比如权限检查、日志记录、缓存等。
远程代理:代理对象在客户端上本地表示,而真实对象在远程服务器上执行。客户端调用代理对象方法,代理通过网络通信调用远程对象的方法,典型场景是分布式系统。
虚拟代理:用于延迟创建代价高的对象或在创建对象时提供一些开销优化。比如大型图片只有在显示时才加载。
保护代理:控制对对象的访问权限,保护敏感操作。保护代理可以确保调用者是否有足够的权限访问对象的某些功能。
智能引用代理:代理会做额外的操作,比如引用计数、资源管理、缓存等。每次访问对象时,代理可以自动处理某些额外的任务。
在这个场景中,我们使用保护代理来控制对用户个人资料的访问权限。具体来说,系统根据不同的用户身份(是本人还是他人)提供不同的访问代理类。
Person
接口:定义了用户的基本信息和操作,包括获取和设置姓名、性别、兴趣以及打分等操作。Person
接口相当于代理模式中的 Subject
,表示所有操作的接口规范。
PersonImpl
类:用户的实际实现类,它包含了用户的基本资料信息。这个类相当于代理模式中的 RealSubject
,是真正存储和处理用户资料的类。
OwnerProxy
类:为用户自己提供的代理类,它允许用户修改自己的资料,但不允许自己为自己打分。这个代理类控制对用户的实际对象(PersonImpl
)的访问权限。
NonOwnerProxy
类:为其他用户提供的代理类,它允许其他用户查看并打分,但不允许他们修改用户的资料。它限制了其他用户对目标对象的访问。
自己代理(OwnerProxy
):该代理类为用户本人提供服务,它允许用户查看和修改自己的个人信息,但限制了用户为自己打分。这是为了防止用户自己提升自己的受欢迎程度(HotOrNotRating
),从而保持评分的公正性。
他人代理(NonOwnerProxy
):该代理类为其他用户提供服务,它允许其他用户查看用户的资料,并且可以为其打分(表示好感度或受欢迎度)。但是,该代理类限制了其他用户对个人信息的修改权限,防止篡改资料。
代理行为:当客户端调用代理对象的方法时(例如调用 setHotOrNotRating()
或 setInterests()
),代理对象会首先检查是否具有执行该操作的权限。如果有权限,它会将操作转发给真实的 PersonImpl
对象,否则会抛出 UnsupportedOperationException
异常。
透明代理:代理类实现了和真实对象相同的接口 Person
,使得客户端代码无须关心它是直接操作的真实对象还是通过代理对象操作,确保客户端代码的一致性和透明性。
控制访问:代理模式能够灵活地控制对真实对象的访问,比如干饭村约会系统中的访问权限控制。
增加额外功能:代理类可以在访问真实对象之前或之后执行一些额外的功能,比如权限校验、日志记录、性能监控、结果缓存等。
解耦客户端和实际对象:代理模式通过代理对象将客户端与真实对象隔离,使得真实对象的更改对客户端的影响最小。
支持远程调用或延迟加载:代理模式允许远程调用或者延迟加载,当对象创建代价较高时,代理可以帮助进行优化。
增加复杂性:代理模式增加了系统的复杂性和间接性,尤其是在系统中有多个代理层时,可能导致代码难以维护和调试。
性能开销:代理模式引入了额外的间接调用,可能带来一定的性能开销,特别是在远程代理或虚拟代理中,网络延迟和额外处理可能会影响性能。
权限控制:需要对某些方法调用进行权限控制,比如干饭村的约会系统中,不同的用户对其他人的资料有不同的操作权限。
远程调用:当对象位于不同的地址空间时,可以使用远程代理模式来处理分布式系统中的通信。
延迟加载:当对象创建代价较高时,使用虚拟代理可以在需要时才创建对象,避免不必要的开销。
日志和缓存:可以使用代理模式在真实对象方法调用前后记录日志或缓存结果,提高性能。
代理模式通过代理对象控制对真实对象的访问,既可以提高系统的安全性和可控性,也可以减少客户端和真实对象之间的耦合。干饭村的约会系统通过代理模式保护用户的个人资料,保证只有自己能够修改资料,而其他人只能查看和打分,从而增强系统的安全性和公平性。
远程调用(Remote Method Invocation, RMI)是一种允许对象在不同的 Java 虚拟机中调用方法的机制,适用于分布式系统。在 RMI 中,客户端可以通过代理(代理模式)调用远程服务器上的方法,而不需要知道方法的具体实现。
我们将通过一个简单的 Java RMI 示例来展示如何进行远程调用。假设我们有一个远程接口 Calculator
,客户端可以调用远程服务器上的加法和减法操作。
Calculator
Calculator
远程接口需要继承 java.rmi.Remote
接口,并声明所有的远程方法可能抛出的 RemoteException
。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Calculator extends Remote {
// 声明远程方法
int add(int a, int b) throws RemoteException;
int subtract(int a, int b) throws RemoteException;
}
远程对象的实现类需要继承 UnicastRemoteObject
,这是为了使对象可以被远程调用。
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
public class CalculatorImpl extends UnicastRemoteObject implements Calculator {
// 必须定义一个构造器,并抛出 RemoteException
protected CalculatorImpl() throws RemoteException {
super();
}
@Override
public int add(int a, int b) throws RemoteException {
return a + b;
}
@Override
public int subtract(int a, int b) throws RemoteException {
return a - b;
}
}
服务器端需要启动 RMI 注册表,并将远程对象绑定到注册表中,使客户端能够通过注册表获取该远程对象。
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class CalculatorServer {
public static void main(String[] args) {
try {
// 启动 RMI 注册表,默认端口是 1099
LocateRegistry.createRegistry(1099);
// 创建远程对象
Calculator calculator = new CalculatorImpl();
// 绑定远程对象到 RMI 注册表
Naming.rebind("rmi://localhost:1099/CalculatorService", calculator);
System.out.println("Calculator Service is running...");
} catch (RemoteException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端通过 RMI 注册表查找远程对象,并调用其方法。
import java.rmi.Naming;
public class CalculatorClient {
public static void main(String[] args) {
try {
// 查找远程对象
Calculator calculator = (Calculator) Naming.lookup("rmi://localhost:1099/CalculatorService");
// 调用远程方法
int sum = calculator.add(5, 3);
int difference = calculator.subtract(10, 4);
System.out.println("Sum: " + sum);
System.out.println("Difference: " + difference);
} catch (Exception e) {
e.printStackTrace();
}
}
}
先编译所有类:
javac Calculator.java CalculatorImpl.java CalculatorServer.java CalculatorClient.java
运行 RMI 注册表:
在命令行中输入:
rmiregistry
注意:RMI 注册表必须在服务器启动之前启动。否则,服务器端程序将无法绑定远程对象。
启动服务器:
在命令行中运行:
java CalculatorServer
运行客户端:
在客户端机器或同一台机器上,运行客户端程序:
java CalculatorClient
当客户端成功调用远程方法时,输出类似如下:
Sum: 8
Difference: 6
Calculator
接口:定义了远程服务的行为,客户端和服务器都需要这个接口。CalculatorImpl
类:这是远程服务的实际实现,服务器将它绑定到 RMI 注册表中。LocateRegistry.createRegistry()
创建一个 RMI 注册表,然后使用 Naming.rebind()
方法将远程对象绑定到特定名称(在这个例子中是 CalculatorService
)。Naming.lookup()
查找远程对象,然后调用远程对象的方法。RMI 底层处理网络通信,客户端可以像调用本地方法一样调用远程方法。延迟加载代理模式(Virtual Proxy)是代理模式的一种,它用于推迟对昂贵资源的加载,直到真正需要时才初始化该资源。这种模式适用于那些初始化成本高但并非总是立即使用的对象,比如图像、文件等。
下面是一个基于延迟加载代理模式的 Java 示例:假设我们有一个图像浏览器,图像可能非常大,所以我们希望在需要显示时才加载图像。
Image
接口:定义图像的行为。RealImage
类:实际的图像类,加载图像的过程可能比较耗时。ImageProxy
类:代理类,只有在需要显示图像时才加载 RealImage
。Client
:客户端使用图像代理来控制加载。Image
接口首先定义 Image
接口,包含一个 display()
方法,用于显示图像。
public interface Image {
void display();
}
RealImage
类RealImage
类是图像的实际实现类,加载过程可能比较耗时,比如从磁盘加载大文件。在构造函数中模拟加载过程。
public class RealImage implements Image {
private String filename;
// 模拟加载过程
public RealImage(String filename) {
this.filename = filename;
loadImageFromDisk();
}
// 模拟图像从磁盘加载的操作
private void loadImageFromDisk() {
System.out.println("Loading image from disk: " + filename);
try {
Thread.sleep(3000); // 模拟耗时的加载
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}
ImageProxy
类(延迟加载代理)代理类 ImageProxy
实现 Image
接口,但它不会立即加载图像,只有在调用 display()
方法时才初始化 RealImage
。
public class ImageProxy implements Image {
private RealImage realImage;
private String filename;
public ImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
// 延迟加载 RealImage,只有在真正需要时才初始化
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
客户端不直接与 RealImage
交互,而是通过代理 ImageProxy
访问图像。
public class Client {
public static void main(String[] args) {
Image image1 = new ImageProxy("photo1.jpg");
Image image2 = new ImageProxy("photo2.jpg");
// 图像还未被加载,此时不显示加载信息
System.out.println("Images are created, but not loaded yet.");
// 当需要显示图像时,图像才会被加载
image1.display(); // 第一次显示图像,加载后再显示
image1.display(); // 第二次显示图像,已经加载过,直接显示
image2.display(); // 第一次显示图像,加载后再显示
}
}
Images are created, but not loaded yet.
Loading image from disk: photo1.jpg
Displaying image: photo1.jpg
Displaying image: photo1.jpg
Loading image from disk: photo2.jpg
Displaying image: photo2.jpg
Image
接口:定义了图像的基本行为 display()
,它是 RealImage
和 ImageProxy
共同的接口。
RealImage
类:这是实际的图像实现类,包含从磁盘加载图像的模拟过程(耗时操作),并实现了 display()
方法来显示图像。
ImageProxy
类:代理类实现了 Image
接口,但它控制 RealImage
的创建和加载。只有在 display()
方法被调用时,ImageProxy
才会创建 RealImage
对象并调用它的 display()
方法。这种设计避免了不必要的图像加载,从而提高了性能。
延迟加载:图像只有在需要显示时才会加载,这就是延迟加载的特点。如果客户端从不调用 display()
方法,图像也不会被加载。
Image
接口,代理类控制实际的加载过程。通过这个示例,延迟加载代理模式 通过代理对象在需要时才实例化真正的对象,避免了不必要的资源浪费,提高了系统性能。
日志代理模式和缓存代理模式都是代理模式的扩展,用于在不改变原始类代码的情况下添加额外的功能。日志代理用于记录方法调用日志,而缓存代理用于减少重复计算或数据获取,提升性能。
下面是基于 日志代理模式 和 缓存代理模式 的示例代码,展示如何使用代理模式记录方法调用日志和缓存计算结果。
假设我们有一个简单的计算器类,进行一些复杂计算。我们将使用日志代理记录每次计算的调用,使用缓存代理存储计算结果以避免重复计算。
Calculator
接口定义一个基本的 Calculator
接口,包含一个用于进行复杂计算的方法 compute()
。
public interface Calculator {
int compute(int a, int b);
}
Calculator
类RealCalculator
类是计算器的实际实现类,它执行复杂的计算操作。在此示例中,假设 compute()
方法执行复杂的乘法运算。
public class RealCalculator implements Calculator {
@Override
public int compute(int a, int b) {
// 假设这是一个耗时的计算
System.out.println("Performing complex calculation...");
return a * b;
}
}
LoggingProxy
LoggingProxy
代理类用于记录每次调用 compute()
方法时的日志信息。该代理类会在方法调用前后打印日志,但不改变原始计算逻辑。
public class LoggingProxy implements Calculator {
private Calculator calculator;
public LoggingProxy(Calculator calculator) {
this.calculator = calculator;
}
@Override
public int compute(int a, int b) {
System.out.println("Logging: About to compute (" + a + ", " + b + ")");
int result = calculator.compute(a, b);
System.out.println("Logging: Computation result is " + result);
return result;
}
}
CachingProxy
CachingProxy
类用于缓存先前的计算结果,以避免重复计算。它使用一个 Map
来存储输入参数和对应的计算结果。
import java.util.HashMap;
import java.util.Map;
public class CachingProxy implements Calculator {
private Calculator calculator;
private Map<String, Integer> cache = new HashMap<>();
public CachingProxy(Calculator calculator) {
this.calculator = calculator;
}
@Override
public int compute(int a, int b) {
String key = a + "," + b;
// 检查缓存中是否已有该计算结果
if (cache.containsKey(key)) {
System.out.println("Cache hit for (" + a + ", " + b + ")");
return cache.get(key);
}
// 如果缓存中没有,进行计算并存入缓存
int result = calculator.compute(a, b);
cache.put(key, result);
return result;
}
}
在客户端中,我们先创建一个 RealCalculator
实例,然后通过代理类进行调用。
public class Client {
public static void main(String[] args) {
// 创建实际的计算器对象
Calculator realCalculator = new RealCalculator();
// 包装成日志代理
Calculator loggingCalculator = new LoggingProxy(realCalculator);
// 包装成缓存代理
Calculator cachingCalculator = new CachingProxy(loggingCalculator);
// 执行一些计算操作
System.out.println("First calculation:");
System.out.println("Result: " + cachingCalculator.compute(3, 4));
System.out.println("\nSecond calculation (same values, should use cache):");
System.out.println("Result: " + cachingCalculator.compute(3, 4));
System.out.println("\nThird calculation (different values):");
System.out.println("Result: " + cachingCalculator.compute(5, 6));
}
}
First calculation:
Logging: About to compute (3, 4)
Performing complex calculation...
Logging: Computation result is 12
Result: 12
Second calculation (same values, should use cache):
Cache hit for (3, 4)
Result: 12
Third calculation (different values):
Logging: About to compute (5, 6)
Performing complex calculation...
Logging: Computation result is 30
Result: 30
RealCalculator
类:这是实际的计算类,它执行真实的计算操作。每次调用 compute()
方法时,它都会进行一次“复杂”的计算(在此示例中为乘法)。
LoggingProxy
类:它是日志代理类,用于记录计算的调用过程。在调用 compute()
方法之前,代理类会记录日志,表示即将开始计算;在调用方法之后,代理类会记录计算结果。
CachingProxy
类:它是缓存代理类,负责缓存先前的计算结果,避免重复计算。如果相同的输入参数之前已经计算过,代理类会直接返回缓存中的结果,而不再调用实际的计算逻辑。
客户端:客户端使用代理类来操作计算器对象,而不直接操作 RealCalculator
。客户端并不关心计算器是直接执行的还是通过代理执行的,只关心计算的结果。