开发知识

Spring一个强大便捷的代理工厂类,你用过吗?

来源: Spring全家桶实战案例源码  日期:2024-04-30 20:54:00  点击:24  属于:开发知识

环境:Spring6.1.2

1. 简介

在Spring框架中,AOP(面向切面编程)是一种强大的编程范式,它允许开发者在不修改原有代码的情况下,为程序添加额外的功能,如日志记录、事务管理、安全控制等。

实际开发中常用实现AOP配置方式:

  • 基于XML

在早期的Spring版本中,开发者常常使用XML配置文件来定义切面、通知和目标对象之间的关联。通过配置<aop:config>、<aop:aspect>、<aop:before>等标签,可以轻松地实现AOP的各种功能。如下示例:

<aop:config>
  <aop:aspect id="myAspect" ref="aBean">
    <aop:pointcut id="businessService"
      expression="execution(* com.pack.service.*.*(..))"/>
    <aop:before pointcut-ref="businessService" method="monitor"/>
  </aop:aspect>
</aop:config>
  • 基于注解

通过在切面类和方法上使用如@Aspect、@Before、@After等注解,可以更加简洁地定义AOP的相关配置。这种方式不仅减少了XML配置的工作量,还使得代码更加清晰易读。如下示例:

@Component
@Aspect
public class LogAspect {
  @Pointcut("execution(* save(..))")
  private void logPc() {}
  @Around("logPc()")
  public Object process(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = null ;
    System.out.println("before log...") ;
    ret = pjp.proceed() ;
    System.out.println("after log...") ;
    return ret ;
  }
}

以上是Spring提供的2中方式来声明AOP配置方式。但如果你需要一种更加灵活和可配置性,那么Spring还提供了一个非常方便强大的ProxyFactoryBean类,该类特别适合那些需要更多自定义和控制的场景,例如当你需要为特定的Bean创建代理,或者需要在不修改原始代码的情况下为现有类添加额外的功能时。

2. 实战案例

ProxyFactoryBean与其他Spring FactoryBean实现一样,引入了一个间接级别。如果定义了名为pack的ProxyFactoryBean,那么引用pack的对象看不到ProxyFactoryBean实例本身,而是由ProxyFactoryBean#getObject()方法实现创建的对象。此方法创建一个AOP代理,用于包装目标对象。

2.1 属性配置

ProxyFactoryBean提供了很多属性,让你可以灵活的配置代理对象。该对象继承了ProxyConfig,一些关键的属性是由ProxyConfig定义。

  • proxyTargetClass:如果要代理目标类,而不是目标类的接口,则为true。如果此属性值设置为true,则会创建CGLIB代理。
  • optimize:控制是否对通过CGLIB创建的代理应用积极的优化。除非完全理解相关AOP代理如何处理优化,否则不应该轻松地使用此设置。目前仅用于CGLIB代理。它对JDK动态代理没有影响。
  • frozen:如果代理配置被冻结,则不再允许更改该配置。此属性的默认值为false,因此允许更改(例如添加额外的通知)。
  • exposeProxy:确定是否应在ThreadLocal中公开当前代理,以便目标可以访问它。如果目标需要获取代理,并且exposeProxy属性设置为true,则该目标可以使用AoPontext.currentProxy()方法获取代理对象。
  • proxyInterface:字符串接口名称的数组。
  • interceptorNames:要应用的Advisor、拦截器或其他建议名称的字符串数组。

接下来将从2方面介绍ProxyFactoryBean的使用,代理接口与代理类。2.2 代理接口

要通过ProxyFactoryBean创建代理,你至少需要涉及到下面几点(类):

  • 需要被代理的目标bean类。
  • 一个Advisor或者Advice,增强部分。
  • 指定要代理的接口。

如下示例:

public interface ICommonDAO {
  void save() ;
}
@Component("commonDAOTarget")
public class CommonDAOImpl implements ICommonDAO {
  @Override
  public void save() {
    System.out.println("save operator...") ;
  }
}
@Component
public class LogInterceptor implements MethodInterceptor {
  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    System.out.println("before log...") ;
    Object ret = invocation.proceed() ;
    System.out.println("after  log...") ;
    return ret ;
  }
}


@Configuration
public class AppConfig {
  @Bean
  // 由于上面已经定义了CommonDAOImpl,而这里的FactoryBean#getObject返回的
  // 也是一个实现了ICommonDAO接口的对象,所以需要加上@Primary
  @Primary
  ProxyFactoryBean commonDAO(@Qualifier("commonDAOTarget") CommonDAOImpl commonDAOTarget) throws Exception {
    ProxyFactoryBean proxy = new ProxyFactoryBean() ;
    proxy.setProxyInterfaces(new Class<?>[] {ICommonDAO.class}) ;
    proxy.setTarget(commonDAOTarget) ;
    proxy.setInterceptorNames("logInterceptor") ;
    return proxy ;
  }
}

测试

ICommonDAO dao = context.getBean(ICommonDAO.class) ;
dao.save() ;
// 输出
before log...
save operator...
after  log...

2.3 代理类

如果我们的目标没有实现接口,那么我们只能通过CGLIB进行代理,通过设置proxyTargetClass属性为true。CGLIB代理通过在运行时生成目标类的子类来工作。Spring将这个生成的子类配置为将方法调用委托给原始目标。如下示例:

@Component("commonDAOTarget")
public class CommonDAO {
  public void save() {
    System.out.println("save operator...") ;
  }
}
@Bean
@Primary
ProxyFactoryBean commonDAO(@Qualifier("commonDAOTarget") CommonDAO commonDAOTarget) throws Exception {
  ProxyFactoryBean proxy = new ProxyFactoryBean() ;
  proxy.setTarget(commonDAOTarget) ;
  proxy.setInterceptorNames("logInterceptor") ;
  // 代理类,可以不设置
  proxy.setProxyTargetClass(true) ;
  return proxy ;
}

查看最终的CommonDAO是否是通过CGLIB代理

CommonDAO dao = context.getBean(CommonDAO.class) ;
System.out.println(dao.getClass()) ;

输出结果

class com.pack.aop.create.ProxyFactoryBeanTest2$CommonDAO$$SpringCGLIB$$1

CGLIB代理通过在运行时生成目标类的子类来工作。但需要注意以下事项:

  • final 类不能被代理,因为它们不能被扩展。
  • final方法无法提供增强,因为它们不能被覆盖。
  • 不能增强private方法,因为它们不能被重写。
  • 不可见的方法,通常是来自不同包的父类中的包私有方法,不能被增强,因为它们实际上是私有的。

2.4 模糊匹配拦截器

在上面配置拦截器时,我们都是指定的具体拦截器,其实我们还可以使用通配符,指定拦截器。如下示例:

@Component("global_log")
public class LogInterceptor implements MethodInterceptor {
}
@Component("global_auth")
public class AuthInterceptor implements MethodInterceptor {
}
// ProxyFactoryBena配置
ProxyFactoryBean commonDAO() throws Exception {
  ProxyFactoryBean proxy = new ProxyFactoryBean() ;
  // 注意:这里的通配符必须是最后,你不能放到其它位置
  proxy.setInterceptorNames("global_*") ;
  return proxy ;
}

以上ProxyFactoryBean在初始化时,会自动查找容器中beanName以global_开头的所有Bean对象。