代理设计模式(Proxy Design Pattern)是一种结构型设计模式,它为其他对象提供一个代理,以控制对这个对象的访问。代理模式可以用于实现懒加载、安全访问控制、日志记录等功能。
在设计模式中,代理模式可以分为静态代理和动态代理。静态代理是指代理类在编译时就已经确定,而动态代理是指代理类在运行时动态生成。
静态代理
缓存代理
缓存代理(Caching Proxy)是一种特殊类型的代理模式,它可以为耗时的操作或者重复的请求提供缓存功能,从而提高程序的执行效率。缓存代理通常会在内部维护一个缓存数据结构,如 HashMap 或者 LinkedHashMap,用来存储已经处理过的请求及其结果。
假设有一个数据查询接口,它从数据库或其他数据源中检索数据。在没有缓存代理的情况下,每次查询都需要访问数据库,这可能会导致较高的资源消耗和延迟。通过引入缓存代理,我们可以将查询结果存储在内存中,从而避免重复查询数据库。
首先,定义一个数据查询接口:
public interface DataQuery {
String query(String queryKey);
}
然后,实现一个真实的数据查询类,它从数据库中检索数据:
public class DatabaseDataQuery implements DataQuery {
@Override
public String query(String queryKey) {
// 查询数据库并返回结果
return "Result from database: " + queryKey;
}
}
创建一个缓存代理类,它实现了 DataQuery 接口,并在内部使用 HashMap 作为缓存:
public class CachingDataQueryProxy implements DataQuery {
private final DataQuery realDataQuery;
private final Map<String, String> cache;
public CachingDataQueryProxy(DataQuery realDataQuery) {
this.realDataQuery = realDataQuery;
cache = new HashMap<>();
}
@Override
public String query(String queryKey) {
String result = cache.get(queryKey);
if (result == null) {
result = realDataQuery.query(queryKey);
cache.put(queryKey, result);
System.out.println("Result retrieved from database and added to cache.");
} else {
System.out.println("Result retrieved from cache.");
}
return result;
}
}
最后,可以在客户端代码中使用缓存代理:
public class Client {
public static void main(String[] args) {
DataQuery realDataQuery = new DatabaseDataQuery();
DataQuery cachingDataQueryProxy = new CachingDataQueryProxy(realDataQuery);
String queryKey = "example_key";
// 第一次查询,从数据库中获取数据并将其缓存
System.out.println(cachingDataQueryProxy.query(queryKey));
// 第二次查询相同的数据,从缓存中获取
System.out.println(cachingDataQueryProxy.query(queryKey));
}
}
通过缓存代理如何提供缓存功能,以提高程序的执行效率。
安全代理
(Security Proxy)是一种代理模式的应用,它用于控制对真实主题对象的访问。通过安全代理,可以实现访问控制、权限验证等安全相关功能。
假设有一个敏感数据查询接口,只有具有特定权限的用户才能访问:
首先,定义一个数据查询接口:
public interface SensitiveDataQuery {
String queryData(String userId);
}
接着,实现一个真实的敏感数据查询类:
public class SensitiveDataQueryImpl implements SensitiveDataQuery {
@Override
public String queryData(String userId) {
// 查询敏感数据并返回结果
return "Sensitive data for user: " + userId;
}
}
创建一个安全代理类,它实现了 SensitiveDataQuery 接口,并在内部进行权限验证:
public class SecurityProxy implements SensitiveDataQuery {
private final SensitiveDataQuery sensitiveDataQuery;
private final UserAuthenticator userAuthenticator;
public SecurityProxy(SensitiveDataQuery sensitiveDataQuery, UserAuthenticator userAuthenticator) {
this.sensitiveDataQuery = sensitiveDataQuery;
this.userAuthenticator = userAuthenticator;
}
@Override
public String queryData(String userId) {
if (userAuthenticator.hasPermission(userId)) {
return sensitiveDataQuery.queryData(userId);
} else {
return "Access Denied: Insufficient permission for user" + userId;
}
}
}
在此示例中,我们使用一个 UserAuthenticator 类来模拟用户权限验证:
public class UserAuthenticator {
private final List<String> authorizedUserIds;
public UserAuthenticator() {
// 模拟从数据库或配置文件中获取已授权的用户列表
authorizedUserIds = Arrays.asList("user1", "user2", "user3");
}
public boolean hasPermission(String userId) {
return authorizedUserIds.contains(userId);
}
}
最后,可以在客户端代码中使用安全代理:
public class Client {
public static void main(String[] args) {
SensitiveDataQuery sensitiveDataQuery = new SensitiveDataQueryImpl();
UserAuthenticator userAuthenticator = new UserAuthenticator();
SensitiveDataQuery securityProxy = new SecurityProxy(sensitiveDataQuery, userAuthenticator);
String userId1 = "user1";
String userId2 = "user4";
// 用户1具有访问权限
System.out.println(securityProxy.queryData(userId1));
// 用户4没有访问权限
System.out.println(securityProxy.queryData(userId2));
}
}
虚拟代理
(Virtual Proxy)是一种代理模式,用于在需要时延迟创建耗时或资源密集型对象。虚拟代理在初始访问时才创建实际对象,之后将直接使用该对象。这可以避免在实际对象尚未使用的情况下就创建它,从而节省资源。
假设有一个大型图片类,它从网络加载图像。由于图像可能非常大,希望在需要显示时才加载它。为了实现这一点,可以创建一个虚拟代理来代表大型图片类。
首先,定义一个图片接口:
public interface Image {
void display();
}
然后,实现一个大型图片类,它从网络加载图像并实现 display()
方法:
public class LargeImage implements Image {
private final String imageUrl;
public LargeImage(String imageUrl) {
this.imageUrl = imageUrl;
loadImageFromNetwork();
}
private void loadImageFromNetwork() {
System.out.println("Loading image from network: " + imageUrl);
// 真实的图像加载逻辑...
}
@Override
public void display() {
System.out.println("Displaying image: " + imageUrl);
}
}
创建一个虚拟代理类,它实现了 Image
接口,并在内部使用 LargeImage
:
public class VirtualImageProxy implements Image {
private final String imageUrl;
private LargeImage largeImage;
public VirtualImageProxy(String imageUrl) {
this.imageUrl = imageUrl;
}
@Override
public void display() {
if (largeImage == null) {
largeImage = new LargeImage(imageUrl);
}
largeImage.display();
}
}
最后,在客户端代码中使用虚拟代理:
public class Client {
public static void main(String[] args) {
Image virtualImageProxy = new VirtualImageProxy("https://example.com/large-image.jpg");
System.out.println("Image will not be loaded until it is displayed.");
// 调用 display() 方法时,才会创建并加载大型图片
virtualImageProxy.display();
}
}
通过虚拟代理如何实现懒加载,以减少资源消耗和提高程序性能。当实际对象的创建和初始化非常耗时或占用大量资源时,虚拟代理是一个很好的选择。
远程代理
(Remote Proxy)是一种代理模式,用于访问位于不同地址空间的对象。远程代理可以为本地对象提供与远程对象相同的接口,使得客户端可以透明地访问远程对象。通常,远程代理需要处理网络通信、序列化和反序列化等细节。我们以后做rpc时也会使用。
首先,定义一个服务接口:
public interface RemoteService {
String fetchData(String dataId);
}
然后,实现一个远程服务类,它在服务器端运行并实现 fetchData()
方法:
public class RemoteServiceImpl implements RemoteService {
@Override
public String fetchData(String dataId) {
// 实际操作,例如从数据库获取数据
return "Data from remote service: " + dataId;
}
}
创建一个远程代理类,它实现了 RemoteService
接口,并在内部处理网络通信等细节:
public class RemoteServiceProxy implements RemoteService {
private final String remoteServiceUrl;
private RemoteService remoteService;
public RemoteServiceProxy(String remoteServiceUrl) {
this.remoteServiceUrl = remoteServiceUrl;
this.remoteService = new RemoteService();
}
@Override
public String fetchData(String dataId) {
// 网络通信、序列化和反序列化等逻辑
System.out.println("Connecting to remote service at: " + remoteServiceUrl);
// 假设我们已经获取到远程服务的数据
String result = remoteService.fetchData(dataId);
System.out.println("Received data from remote service.");
return result;
}
}
在客户端代码中使用远程代理:
public class Client {
public static void main(String[] args) {
RemoteService remoteServiceProxy = new RemoteServiceProxy("https://example.com/remote-service");
// 使用远程代理访问远程服务
String dataId = "example_data_id";
String result = remoteServiceProxy.fetchData(dataId);
System.out.println("Result: " + result);
}
}
远程代理的基本概念,省略了实际的网络通信、序列化和反序列化逻辑。在实际应用中,可以使用 Java RMI、gRPC 或其他远程通信库来实现远程代理。
静态代理步骤总结大致流程如下:
- 创建一个接口,定义代理类和被代理类共同实现的方法。
- 创建被代理类,实现这个接口,并且在其中定义实现方法。
- 创建代理类,也要实现这个接口,同时在其中定义一个被代理类的对象作为成员变量。
- 在代理类中实现接口中的方法,方法中调用被代理类中的对应方法。
- 通过创建代理对象,并调用其方法,方法增强。
这样,被代理类的方法就会被代理类所覆盖,实现了对被代理类的增强或修改。
当然在静态代理中,也可以使用继承来实现代理。具体步骤如下:
- 创建被代理类,定义需要被代理的方法。
- 创建代理类,继承被代理类,重写被代理类中的方法,对方法进行增强。
- 在重写的方法中添加代理逻辑,例如在调用被代理类中的方法前后添加日志记录、安全检查等功能。
- 在使用代理类时,创建代理类的对象,调用重写的方法。
这样,被代理类的方法就会被代理类所覆盖,实现了对被代理类的增强或修改。使用继承来实现代理的好处是简单易懂,不需要创建接口,同时继承可以继承被代理类的属性和方法,可以更方便地访问被代理类中的成员。但是,这种方式也有一些缺点,例如代理类与被代理类的耦合度较高,不够灵活。
动态代理
Java 中动态代理的实现方式主要有两种:基于 JDK 的动态代理和基于 CGLIB 的动态代理。
静态代理需要手动编写代理类,代理类与被代理类实现相同的接口或继承相同的父类,对被代理对象进行包装。在程序运行前,代理类的代码就已经生成,并在程序运行时调用。静态代理的优点是简单易懂,缺点是需要手动编写代理类,代码复杂度较高,且不易扩展。
动态代理是在程序运行时动态生成代理类,无需手动编写代理类,大大降低了代码的复杂度。动态代理一般使用 Java 提供的反射机制实现,可以对任意实现了接口的类进行代理。动态代理的优点是灵活性高,可以根据需要动态生成代理类,缺点是性能相对较低,由于使用反射机制,在运行时会产生额外的开销。
总之,静态代理和动态代理都是代理模式的实现方式,其主要区别在于代理类的生成时机和方式。静态代理需要手动编写代理类,适用于代理类数量较少、不需要频繁修改的场景。而动态代理不需要手动编写代理类,可以动态生成代理类,适用于代理类数量较多、需要频繁修改的场景。
基于 JDK 的动态代理实现步骤
基于 JDK 的动态代理需要使用 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口。
依旧使用缓存代理的案例来实现,具体步骤如下:
(1)定义一个接口,声明需要代理的方法:
public interface DataQuery {
String query(String queryKey);
String queryAll(String queryKey);
}
(2)创建一个被代理类,实现这个接口,并在其中定义实现方法:
public class DatabaseDataQuery implements DataQuery {
@Override
public String query(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "result";
}
@Override
public String queryAll(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "all result";
}
}
(3)创建一个代理类,实现 InvocationHandler
接口,并在其中定义一个被代理类的对象作为属性。
public class CacheInvocationHandler implements InvocationHandler {
private HashMap<String,String> cache = new LinkedHashMap<>(256);
private DataQuery databaseDataQuery;
public CacheInvocationHandler(DatabaseDataQuery databaseDataQuery) {
this.databaseDataQuery = databaseDataQuery;
}
public CacheInvocationHandler() {
this.databaseDataQuery = new DatabaseDataQuery();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1、判断是哪一个方法
String result = null;
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);
}
}
在代理类中,实现了 InvocationHandler
接口,并在其中定义了一个被代理类的对象作为属性。在 invoke
方法中,可以对被代理对象的方法进行增强,并在方法调用前后输出日志。
(4)在使用代理类时,创建被代理类的对象和代理类的对象,并使用 Proxy.newProxyInstance
方法生成代理对象。
public class Main {
public static void main(String[] args) {
// 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);
System.out.println("--------------------");
result = dataQuery.query("key1");
System.out.println(result);
System.out.println("--------------------");
result = dataQuery.query("key2");
System.out.println(result);
System.out.println("++++++++++++++++++++++++++++++++++++");
// 事实上调用queryAll方法的使用,他是调用了invoke
result = dataQuery.queryAll("key1");
System.out.println(result);
System.out.println("--------------------");
result = dataQuery.queryAll("key1");
System.out.println(result);
System.out.println("--------------------");
result = dataQuery.queryAll("key2");
System.out.println(result);
System.out.println("--------------------");
}
}
使用 Proxy.newProxyInstance
方法生成代理对象,并将代理对象转换成 DataQuery
接口类型,以便调用其代理的方法。在代理对象调用方法时,会调用 CacheInvocationHandler
类中的 invoke
方法,实现对被代理对象的方法的增强。
基于 CGLIB 的动态代理实现步骤
基于 CGLIB 的动态代理需要使用 net.sf.cglib.proxy.Enhancer
类和 net.sf.cglib.proxy.MethodInterceptor
接口。具体步骤如下:
(1)创建一个被代理类,定义需要被代理的方法。
public class DatabaseDataQuery {
public String query(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "result";
}
public String queryAll(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "all result";
}
}
(2)创建一个方法拦截器类,实现 MethodInterceptor
接口,并在其中定义一个被代理类的对象作为属性。
public class CacheMethodInterceptor implements MethodInterceptor {
private HashMap<String,String> cache = new LinkedHashMap<>(256);
private DatabaseDataQuery databaseDataQuery;
public CacheMethodInterceptor() {
this.databaseDataQuery = new DatabaseDataQuery();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 1、判断是哪一个方法
String result = null;
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);
}
}
在这个代理类中,实现了 MethodInterceptor
接口,并在其中定义了一个被代理类的对象作为属性。在 intercept
方法中,我们可以对被代理对象的方法进行增强,并在方法调用前后输出日志。
(3)在使用代理类时,创建被代理类的对象和代理类的对象,并使用 Enhancer.create
方法生成代理对象。
public class Main {
public static void main(String[] args) {
// cglib通过Enhancer
Enhancer enhancer = new Enhancer();
// 设置他的父类
enhancer.setSuperclass(DatabaseDataQuery.class);
// 设置一个方法拦截器
enhancer.setCallback(new CacheMethodInterceptor());
// 创建代理类
DatabaseDataQuery databaseDataQuery = (DatabaseDataQuery)enhancer.create();
databaseDataQuery.query("key1");
databaseDataQuery.query("key1");
databaseDataQuery.query("key2");
}
}
使用 Enhancer.create
方法生成代理对象,并将代理对象转换成 RealSubject
类型,以便调用 request
方法。在代理对象调用 request
方法时,会调用 DynamicProxy
类中的 intercept
方法,实现对被代理对象的增强。
在实际应用中,基于 CGLIB 的动态代理可以代理任意类,但是生成的代理类比较重量级。如果被代理类是一个接口,建议使用基于 JDK 的动态代理来实现,这也是spring的做法;如果被代理类没有实现接口或者需要代理的方法是 final 方法,建议使用基于 CGLIB 的动态代理来实现。
spring中aop的使用步骤
在 Spring 中,AOP(面向切面编程)提供了一种有效的方式来对程序中的多个模块进行横切关注点的处理,例如日志、事务、缓存、安全等。使用 Spring AOP,可以在程序运行时动态地将代码织入到目标对象中,从而实现对目标对象的增强。
Spring AOP 的使用步骤如下:
(1)引入 AOP 相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
(2)开启自动代理@EnableAspectJAutoProxy
@SpringBootApplication
@EnableAspectJAutoProxy
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
(3)定义接口和实现类,并将具体实现注入容器:
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)定义切面类,对方法做增强
@Component
@Aspect
public class CacheAspectj {
@Pointcut("execution(* com.ydlclass.proxy.dynamicProxy.aop.DatabaseDataQuery.query(..))")
public void pointcut() {}
@Around("pointcut()")
public String around(ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
String key = args[0].toString();
// 1、查询缓存,命中则返回
String result = Cache.get(key);
if(result != null){
System.out.println("数据从缓存中获取");
return result;
}
// 未命中则去数据库查询,实际上是调用被代理bean的方法
try {
result = joinPoint.proceed().toString();
// 如果查询有结果,进行缓存
Cache.put(key,result);
} catch (Throwable e) {
throw new RuntimeException(e);
}
return result;
}
}
定义缓存
public class Cache {
private static Map<String,String> map = new ConcurrentHashMap<>(256);
public static String get(String key){
return map.get(key);
}
public static void put(String key,String value){
map.put(key, value);
}
}
(5)测试:
在使用增强后的 Bean 时,Spring AOP 会自动代理这些 Bean,并在方法调用前后调用相应的通知方法。例如:
@SpringBootTest
public class AopTest {
@Resource
private DataQuery dataQuery;
@Test
public void testAop(){
dataQuery.query("key1");
dataQuery.query("key1");
dataQuery.query("key2");
}
}
定义了一个名为 MyService
的服务类,并使用 @Service
注解将其添加到 Spring 容器中。由于我们已经添加了名为 loggingAspect
的切面,因此在调用 doSomething
方法时,Spring AOP 会自动增强这个方法,调用前置通知和后置通知。
这就是使用 Spring AOP 的基本步骤。需要注意的是,在配置切点时,我们可以使用多种方式来定义切点,例如基于表达式、基于注解、基于 XML 配置等。具体选择哪种方式,取决于项目的实际需求和开发者的个人习惯。
在使用 Spring AOP 时,还需要注意以下几点:
- 如果目标对象实现了接口,则默认使用 JDK 动态代理进行代理;如果目标对象没有实现接口,则使用 CGLIB 进行代理。如果想要强制使用 CGLIB 进行代理,可以在配置类中使用
@EnableAspectJAutoProxy(proxyTargetClass = true)
注解。 - 前置通知、后置通知、环绕通知等通知类型都可以使用
@Before
、@After
、@Around
等注解进行定义。在通知方法中,可以通过JoinPoint
参数获取目标方法的参数等信息。 - 如果需要将通知方法定义在不同的切面中,可以使用
@Order
注解指定切面的执行顺序。 - 如果需要在同一个通知方法中定义多个切点,可以使用
&&
和||
等逻辑运算符来组合切点表达式。
总的来说,Spring AOP 是一种非常方便的 AOP 实现方式,可以大大简化程序中的横切关注点处理,提高代码的可重用性和可维护性。
面向切面编程(AOP)和代理是两个相关但不同的概念。
代理是一种结构型设计模式,它为其他对象提供一种代理以控制对这个对象的访问。代理模式可以分为静态代理和动态代理两种。
在静态代理中,代理类和被代理类都必须实现同一个接口,代理类通过实现接口的方式来实现被代理类的功能,并在调用被代理类的方法前后执行一些额外的逻辑。在动态代理中,代理类是在运行时动态生成的,不需要实现接口,通过 Java 反射机制来动态地生成代理对象,从而实现对被代理类的增强。
AOP 是一种编程范式,它通过对程序中的**多个模块进行横切关注点的处理,实现对程序行为的增强。AOP 的实现方式可以有多种,例如基于代理、基于字节码增强、基于注解等。**其中基于代理的 AOP 实现方式,也就是 Spring AOP,是一种常见的 AOP 实现方式。
在 Spring AOP 中,切面是 AOP 的核心概念,它由切点和通知组成。切点定义了哪些方法或类需要被增强,通知则定义了增强的具体逻辑。在基于代理的 AOP 实现方式中,代理类负责在调用被代理对象的方法前后调用相应的通知方法,从而实现对被代理对象的增强。
因此,**代理和 AOP 是两个相关的概念,代理是 AOP 的一种实现方式,**它们都可以用于在程序中实现对目标对象的增强。区别在于,代理主要是针对单个对象的方法调用进行增强,而 AOP 则是针对程序中多个模块的横切关注点进行增强。
动态代理的应用场景
动态代理是一种代理模式,它在运行时动态生成代理对象,而无需提前创建具体的代理类。动态代理在 Java 中通常使用 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口来实现。以下是一些动态代理的使用场景:
- 日志记录: 使用动态代理可以在方法调用前后自动添加日志记录,从而跟踪方法的执行过程。这样可以方便地监控系统运行情况,诊断问题,而无需修改实际类的代码。
- 性能监控: 动态代理可用于测量方法执行的时间,以评估性能。在方法调用前后记录时间戳,然后计算时间差,就可以得到方法执行所需的时间。
- 事务管理: 在数据库操作中,动态代理可用于自动管理事务。在方法调用前开始一个事务,在方法成功执行后提交事务,如果发生异常,则回滚事务。这样可以确保数据的一致性和完整性。
- 权限验证: 使用动态代理可以在方法调用前进行权限验证,确保只有具有适当权限的用户才能访问受保护的资源。这可以提高系统的安全性。
- 缓存: 动态代理可用于实现方法结果的缓存。在方法调用前检查缓存,如果缓存中有结果,则直接返回,否则执行方法并将结果存入缓存。这可以提高程序的执行效率。
- 负载均衡与故障转移: 在分布式系统中,动态代理可以用于实现负载均衡和故障转移。代理对象根据某种策略(如轮询、随机等)选择一个可用的服务实例,并将请求转发给它。如果服务实例发生故障,代理对象可以自动选择另一个可用的实例。
- API 速率限制: 使用动态代理,可以在方法调用前检查 API 请求速率是否超过预设的限制。如果超过限制,可以拒绝请求或将请求延迟一段时间后再执行。
- 数据验证: 在方法调用前,动态代理可以用于验证传入的参数是否符合预期的规则和约束。这有助于确保数据的有效性和一致性。
- 重试机制: 当方法调用失败时(例如,因为网络问题、服务不可用等原因),动态代理可以实现自动重试的机制。代理对象可以在一定的时间间隔内尝试重新执行方法,直到成功或达到最大重试次数。
- 懒加载与资源管理: 动态代理可以用于实现资源的懒加载和管理。例如,代理对象可以在第一次访问资源时才创建和初始化它。此外,代理对象还可以在资源不再需要时自动释放它,以减少内存占用和提高性能。
- 跨语言和跨平台调用: 动态代理可以实现跨语言和跨平台的对象调用。例如,一个 Java 客户端可以使用动态代理调用一个基于 Python 的服务。在这种情况下,代理对象会负责处理跨语言通信的细节,如序列化、反序列化和网络传输。
- AOP(面向切面编程): 动态代理是实现 AOP 的一种方式。AOP 允许在程序运行时动态地插入和修改横切关注点(如日志记录、性能监控等),而无需修改实际代码。动态代理可以轻松地实现 AOP,以提高代码的可维护性和可重用性。
这些仅仅是动态代理的一些应用场景。动态代理是一种非常灵活的技术,可以用于解决许多不同类型的问题。在实际项目中,根据具体需求,可以将动态代理与其他设计模式结合使用。
事实上,我们如果不是编写中间件产品,绝大部分的场景都是在spring环境下实现的,我们理解了代理的本是就是增强,他是aop的一种实现方式,工作中绝大部分的场景都是使用aop来实现的。接下来,我们列举几个例子来详细介绍aop的使用场景。
API 速率限制
这是一个使用 Spring AOP 实现 API 速率限制的简单示例。我们将使用 Spring Boot 和 Spring AOP。首先,确保你的项目中包含以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建一个自定义注解 RateLimiter
,用于标记需要进行速率限制的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
int value() default 1;
int durationInSeconds() default 1;
}
需要创建一个切面类(Aspect),实现速率限制的逻辑。这里使用 Google Guava 提供的 RateLimiter
类
@Aspect
@Component
public class RateLimiterAspect {
private final ConcurrentHashMap<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();
@Pointcut("@annotation(rateLimiterAnnotation)")
public void rateLimiterPointcut(RateLimiter rateLimiterAnnotation) {
}
@Around("rateLimiterPointcut(rateLimiterAnnotation)")
public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiterAnnotation) throws Throwable {
int permits = rateLimiterAnnotation.value();
int durationInSeconds = rateLimiterAnnotation.durationInSeconds();
// 使用方法签名作为 RateLimiter 的 key
String key = joinPoint.getSignature().toLongString();
com.google.common.util.concurrent.RateLimiter rateLimiter = rateLimiters.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create((double) permits / durationInSeconds));
// 尝试获取令牌,如果获取到则执行方法,否则抛出异常
if (rateLimiter.tryAcquire()) {
return joinPoint.proceed();
} else {
throw new RuntimeException("Rate limit exceeded.");
}
}
}
在这个切面类中,定义了一个名为 rateLimiterPointcut
的切入点,用于匹配带有 @RateLimiter
注解的方法。around
方法用 @Around
注解标注,实现了速率限制的逻辑。我们使用 ConcurrentHashMap 存储 RateLimiter 实例,以便在多线程环境下安全地访问它们。
在需要进行速率限制的方法上添加 @RateLimiter
注解。例如,以下是一个简单的 REST 控制器:
@RestController
public class ApiController {
@GetMapping("/api/limited")
@RateLimiter(value = 10, durationInSeconds = 60) //限制为每分钟 10 次请求
public String limitedEndpoint() {
return "This API has a rate limit of 10 requests per minute.";
}
@GetMapping("/api/unlimited")
public String unlimitedEndpoint() {
return "This API has no rate limit.";
}
}
重试机制
下面是使用 Spring AOP 实现重试机制的一个示例。首先,创建一个自定义注解 Retry
,用于标记需要进行重试的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int maxAttempts() default 3;
long delayInMillis() default 1000;
Class<? extends Throwable>[] retryOn() default {Throwable.class};
}
需要创建一个切面类(Aspect),实现重试的逻辑:
@Aspect
@Component
public class RetryAspect {
@Pointcut("@annotation(retryAnnotation)")
public void retryPointcut(Retry retryAnnotation) {
}
@Around("retryPointcut(retryAnnotation)")
public Object around(ProceedingJoinPoint joinPoint, Retry retryAnnotation) throws Throwable {
int maxAttempts = retryAnnotation.maxAttempts();
long delayInMillis = retryAnnotation.delayInMillis();
Set<Class<? extends Throwable>> retryOn = new HashSet<>(Arrays.asList(retryAnnotation.retryOn()));
int attempts = 0;
while (true) {
try {
attempts++;
return joinPoint.proceed();
} catch (Throwable throwable) {
if (attempts >= maxAttempts || !retryOn.contains(throwable.getClass())) {
// 此处可以记录重试日志
throw throwable;
}
// 等待一段时间后重试
try {
Thread.sleep(delayInMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw throwable;
}
}
}
}
}
在这个切面类中,定义了一个名为 retryPointcut
的切入点,用于匹配带有 @Retry
注解的方法。around
方法用 @Around
注解标注,实现了重试的逻辑。当方法抛出异常时,检查异常类型是否需要重试,以及是否达到最大重试次数。如果满足重试条件,则等待一段时间后再次尝试执行方法。
在需要进行重试的方法上添加 @Retry
注解。例如,以下是一个简单的服务类:
@Service
public class DemoService {
@Retry(maxAttempts = 5, delayInMillis = 500, retryOn = {RuntimeException.class})
public String retryableMethod() {
System.out.println("Executing retryableMethod...");
throw new RuntimeException("An error occurred");
}
public String nonRetryableMethod() {
System.out.println("Executing nonRetryableMethod...");
return "This method does not have a retry mechanism.";
}
}
在这个示例中,为 retryableMethod
方法设置了重试机制(最多重试 5 次,每次间隔 500 毫秒,仅在发生RuntimeException的时候进行重试。当然重试的时间可以动态调整,如第一次5秒,第二次10秒...
日志记录
使用 Spring AOP 实现日志记录的示例如下。首先,确保你的项目中包含以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建一个切面类(Aspect)来实现日志记录的逻辑:
@Aspect
@Component
public class LoggingAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void restControllerMethods() {
}
@Before("restControllerMethods()")
public void logMethodCall(JoinPoint joinPoint) {
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
logger.info("Entering method [{}.{}]", className, methodName);
}
@AfterReturning(pointcut = "restControllerMethods()", returning = "result")
public void logMethodReturn(JoinPoint joinPoint, Object result) {
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
logger.info("Exiting method [{}.{}], return value: {}", className, methodName, result);
}
}
在这个切面类中,定义了一个名为 restControllerMethods
的切入点,用于匹配所有带有 @RestController
注解的类中的方法。使用 @Before
注解标注 logMethodCall
方法,以便在方法调用前记录日志。类似地,使用 @AfterReturning
注解标注 logMethodReturn
方法,以便在方法成功返回后记录日志。
这个示例使用了 SLF4J 作为日志记录库,可以根据自己的需求更换其他日志记录库。请确保项目中包含 SLF4J 及其所需的依赖。
以下是一个简单的 REST 控制器示例,它将自动记录方法调用和返回的日志:
@RestController
public class SampleController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
当你访问 /hello
端点时,控制台将输出如下日志:
Entering method [SampleController.hello]
Exiting method [SampleController.hello], return value: Hello, World!
使用 Spring AOP 实现简单的日志记录。可以根据实际需求调整日志记录级别、格式和内容,以及扩展切入点表达式以覆盖更多的方法。
实现数据校验
要使用 Spring AOP 实现数据校验,可以创建一个切面类,对方法的输入参数进行校验。这里将使用 JSR 380(Java Bean Validation 2.0)规范实现数据校验。首先确保项目中包含以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后,创建一个自定义注解 Validate
,用于标记需要进行数据校验的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validate {
}
创建一个切面类(Aspect),实现数据校验逻辑:
@Aspect
@Component
public class ValidationAspect {
@Autowired
private Validator validator;
@Pointcut("@annotation(Validate)")
public void validationPointcut() {
}
@Before("validationPointcut()")
public void validateMethodArguments(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
Errors errors = new BeanPropertyBindingResult(arg, arg.getClass().getName());
validator.validate(arg, errors);
if (errors.hasErrors()) {
throw new ValidationException("Validation failed: " + errors.toString());
}
}
}
}
在这个切面类中,定义了一个名为 validationPointcut
的切入点,用于匹配带有 @Validate
注解的方法。使用 @Before
注解标注 validateMethodArguments
方法,在方法调用前进行数据校验。如果校验失败,将抛出一个 ValidationException
异常。
在需要进行数据校验的方法上添加 @Validate
注解。例如,以下是一个简单的服务类:
@Service
public class DemoService {
@Validate
public void processData(@Valid DataModel data) {
// Process data
}
}
public class DataModel {
@NotNull
private String field1;
@Size(min = 5, max = 10)
private String field2;
// Getters and setters
}
为 processData
方法添加了 @Valid
注解,以便在方法调用时自动进行数据校验。如果校验失败,切面类将抛出一个异常,从而阻止方法的执行。 这个示例展示了如何使用 Spring AOP 实现数据校验。可以根据实际需求调整校验规则,以及自定义异常处理逻辑。